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

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);
})();

That function injects its own block straight into , separate from the theme's linked stylesheet. And critically — it didn't have the dark mode rule in it. My external style.css did, but this JS-injected block was landing in the DOM after it, with matching specificity, and just... won.

Not a specificity war in the classic sense. Just source order. The JS block gets appended on demand, every time a voice message shows up, and it always lands last in . Last write wins.

The actual fix

Once I saw it, it was almost boring: the dark mode rule needed to live inside the JS-injected style.textContent block, not in the external stylesheet, because that's the block that's actually deciding this element's fate at runtime.

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

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

That's it. Same rule, different home. Worked immediately.

The actual lesson

If your styles are partly generated by JS at runtime — dynamically injected

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论