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