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.
Top comments (0)