# The CSS bug that taught me JS-injected styles always win

> Source: <https://dev.to/nogandev/the-css-bug-that-taught-me-js-injected-styles-always-win-2io2>
> Published: 2026-08-23 17:20:27+00:00

I spent way longer than I'd like to admit chasing a dark mode bug that made zero sense on paper.

I'm building WidgetForge, a drop-in AI chat widget you can paste into any site — static HTML or Next.js, pick a theme, done. Four themes, one shared JS core. Nothing exotic.

Except one small piece of it kept breaking dark mode, and I couldn't figure out why.

The setup

Every message in the chat has little icons — a voice note badge, status icons, that kind of thing. I wanted those to invert properly in dark mode, so I wrote the obvious CSS:

```
@media (prefers-color-scheme: dark) {
  .voice-message-icon {
    filter: invert(1) brightness(2);
  }
}
```

Dropped it in the theme's style.css. Tested it. Worked fine in isolation.

Then I wired it into the actual widget and dark mode just... didn't apply. Same class name, same media query, same browser. No console errors. No typos I could find. It was the kind of bug where everything looks correct, which is the most annoying kind.

Where it actually was

Here's the thing I didn't clock at first: this specific icon isn't rendered from the static HTML at all. It's built at runtime, in JS, when a voice message gets added to the chat:

```
(function injectVoiceMessageStyles() {
  if (document.getElementById("voice-message-badge-styles")) {
    return;
  }

  const style = document.createElement("style");
  style.id = "voice-message-badge-styles";

  style.textContent = `
    .voice-message-icon {
      width: 16px;
      height: 16px;
      object-fit: contain;
      flex-shrink: 0;
    }
  `;

  document.head.appendChild(style);
})();
```

If your styles are partly generated by JS at runtime — dynamically injected `<style>`

tags, CSS-in-JS, whatever — treat that as its own independent stylesheet. It doesn't matter how well-organized your "real" CSS file is if a script is appending a competing block after it loads. External stylesheets are static and load once; JS-injected blocks can show up whenever, and they'll happily override anything sitting above them in the cascade.

Once I knew to look for this pattern, I found two more spots in the same codebase doing the same thing — dynamically created UI elements with their own injected styles that had quietly drifted out of sync with the main theme file. Same fix each time: move the state-dependent rules into the block that's actually authoritative for that element.

Small bug, but it changed how I think about where styling logic should live once JS starts generating markup at runtime.

WidgetForge is a self-hosted AI chat widget — static HTML or Next.js, four themes, no database, no build step.
