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…
I found myself tangled in a perplexing dark mode bug that refused to make logical sense. I was developing WidgetForge, a plug-and-play AI chat widget compatible with any site, be it static HTML or Next.js. The theme had four options, all relying on a single shared JS core. Despite the simplicity, one element consistently malfunctioned in dark mode, and I couldn't pinpoint the issue. The structure:
Every chat message featured icons like voice notes and status indicators. I desired these to invert correctly in dark mode, so I implemented the following CSS rule: @media (prefers-color-scheme: dark) { .voice-message-icon { filter: invert(1) brightness(2); } } I placed it within the appropriate style.css file. Upon testing, the fix worked perfectly when isolated.
However, once integrated into the actual widget, dark mode failed to take effect. Identical class name, same media query, same browser – no console errors, no typos. The problem was elusive, frustrating. After a moment of reflection, I realized the root of the issue:
The icon in question wasn't rendered from static HTML at all. Instead, it was dynamically generated at runtime via JavaScript, specifically when a voice message was 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 my styles were generated dynamically through JavaScript, they should be treated as separate stylesheets.
Regardless of how well-organized the main CSS file was, any dynamically injected block would override it in the cascade. Having identified this pattern, I discovered two more instances in the same codebase exhibiting the same behavior – runtime-generated elements with their own injected styles that had deviated from the main theme.
Recognizing this, I resolved the issue by incorporating state-dependent rules into the block authoritative for that element. While seemingly trivial, this insight significantly altered my understanding of where styling logic should reside when JavaScript dynamically generates markup. WidgetForge, a self-hosted AI chat widget, operates with static HTML or Next.js, four themes, and no database or build step. Live demo available.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.