{"slug": "the-css-bug-that-taught-me-js-injected-styles-always-win", "title": "The CSS bug that taught me JS-injected styles always win", "summary": "A developer building WidgetForge, a self-hosted AI chat widget, encountered a dark mode bug caused by JS-injected styles overriding external CSS. The developer found that dynamically injected <style> tags act as independent stylesheets and can override static CSS, and recommends moving state-dependent rules into the authoritative block for each element.", "body_md": "I spent way longer than I'd like to admit chasing a dark mode bug that made zero sense on paper.\n\nI'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.\n\nExcept one small piece of it kept breaking dark mode, and I couldn't figure out why.\n\nThe setup\n\nEvery 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:\n\n```\n@media (prefers-color-scheme: dark) {\n  .voice-message-icon {\n    filter: invert(1) brightness(2);\n  }\n}\n```\n\nDropped it in the theme's style.css. Tested it. Worked fine in isolation.\n\nThen 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.\n\nWhere it actually was\n\nHere'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:\n\n```\n(function injectVoiceMessageStyles() {\n  if (document.getElementById(\"voice-message-badge-styles\")) {\n    return;\n  }\n\n  const style = document.createElement(\"style\");\n  style.id = \"voice-message-badge-styles\";\n\n  style.textContent = `\n    .voice-message-icon {\n      width: 16px;\n      height: 16px;\n      object-fit: contain;\n      flex-shrink: 0;\n    }\n  `;\n\n  document.head.appendChild(style);\n})();\n```\n\nIf your styles are partly generated by JS at runtime — dynamically injected `<style>`\n\ntags, 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.\n\nOnce 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.\n\nSmall bug, but it changed how I think about where styling logic should live once JS starts generating markup at runtime.\n\nWidgetForge is a self-hosted AI chat widget — static HTML or Next.js, four themes, no database, no build step.", "url": "https://wpnews.pro/news/the-css-bug-that-taught-me-js-injected-styles-always-win", "canonical_source": "https://dev.to/nogandev/the-css-bug-that-taught-me-js-injected-styles-always-win-2io2", "published_at": "2026-08-23 17:20:27+00:00", "updated_at": "2026-08-23 17:43:38.910879+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["WidgetForge"], "alternates": {"html": "https://wpnews.pro/news/the-css-bug-that-taught-me-js-injected-styles-always-win", "markdown": "https://wpnews.pro/news/the-css-bug-that-taught-me-js-injected-styles-always-win.md", "text": "https://wpnews.pro/news/the-css-bug-that-taught-me-js-injected-styles-always-win.txt", "jsonld": "https://wpnews.pro/news/the-css-bug-that-taught-me-js-injected-styles-always-win.jsonld"}}