TL;DR: I shipped a "light mode for the bingo caller" feature three times. Each attempt rendered as dark navy squares on a white stage — totally unreadable. The bug was the same every time: an inline style={{ background: hue.deep }} in a React component winning over the CSS class meant to control the background. Moving from inline styles to inline CSS custom properties unlocked a theme-aware cascade and finally made the feature ship.
The setup
I'm building a number bingo caller — the screen that flashes numbers across a board as they're called. Each column has a hue (Blue B, Red I, Green N, Orange G, Purple O). When a number is "called," that cell lights up in its column's color.
Dark mode shipped first. The visual goal: each called tile is a solid deep block — navy, crimson, forest, pumpkin, royal purple. Looks like a Las Vegas bingo board lit from inside. The implementation was the obvious one:
<button
className="caller-tile caller-tile-called"
style={{ background: hue.deep }} // ← the bug, but I didn't know yet
>
{num}
</button>
Worked great. Shipped.
The light-mode regression
Six weeks later I tried to add light mode. Visual goal: each called tile becomes a vibrant gradient lit from above — the same hue but the light end of its range, not the dark end. Think a bright pop instead of a moody glow.
I wrote the CSS:
.caller-tile-called {
/* Light mode: linear gradient from highlight to mid */
background: linear-gradient(180deg, var(--hue-highlight) 0%, var(--hue-mid) 100%);
}
.dark .caller-tile-called {
/* Dark mode: solid deep */
background: var(--hue-deep);
}
Set the CSS variables on each cell:
<button
className="caller-tile caller-tile-called"
style={{
background: hue.deep, // ← still here
'--hue-highlight': hue.highlight,
'--hue-mid': hue.mid,
'--hue-deep': hue.deep,
}}
>
{num}
</button>
Reloaded the page in light mode. Dark navy squares. Crimson. Forest green. On a stark white stage.
I tried a few "make it work" patches. Increased CSS specificity. Wrapped the rule in :where(.light). Added !important. Nothing worked. The inline style={{ background: hue.deep }} was winning over my CSS rule every time.
I reverted the attempt and stayed in dark mode for another four weeks.
The two more attempts that broke the same way
Each time I came back to it I came up with a new theory:
-
Attempt 2: rewrite the CSS to target both
.light .caller-tile-calledand:not(.dark) .caller-tile-called. Same bug. Inline style still won. -
Attempt 3: rewrite Flashboard to compute the gradient inline and conditionally render it. Forced me to thread theme state through three layers of React just to set a
background:value. Also looked horrible because the inline gradient didn't get thevar()indirection I wanted for component-internal tweaks.
After the third attempt I sat back and read the CSS specification on declared styles carefully.
Inline style attributes have higher specificity than ANY external stylesheet rule, including !important rules in stylesheets (unless the inline style is also !important). My background: var(--hue-deep) was authoring an inline declaration that simply couldn't lose. Every CSS rule I wrote was a bystander.
The fix was straightforward once I saw it: stop authoring background: in JSX entirely. Move the value into a CSS variable that the stylesheet picks up.
The shape that finally shipped
<button
className="caller-tile caller-tile-called"
style={{
'--hue-highlight': hue.highlight,
'--hue-mid': hue.mid,
'--hue-deep': hue.deep,
'--hue-glow': hue.glow,
} as CSSProperties}
>
{num}
</button>
.caller-tile-called {
background: linear-gradient(180deg, var(--hue-highlight) 0%, var(--hue-mid) 100%);
border-color: var(--hue-mid);
}
.dark .caller-tile-called {
background: var(--hue-deep);
border-color: var(--hue-mid);
}
The inline style now only sets CUSTOM PROPERTIES. Custom properties don't paint anything — they're just named values. The CSS rule consumes them and decides what to paint, and the CSS rule can switch between light and dark cases because it's the only thing actually authoring background:.
Light mode: gradient. Dark mode: solid deep. Both branches share the same hue palette. Switching theme is one class flip on <html>.
What I'd do differently from day one
I should have known this. I've answered Stack Overflow questions on inline-style specificity. The reason I missed it in my own code is that inline style={{ ... }} in React is so culturally adjacent to "just set the prop" that I didn't think of it as authoring a CSS declaration. It read as data, not as a stylesheet author.
The general rule I now keep on a sticky note: inline style should set values, not paint properties. CSS custom properties on the element are fine — they're values. Hex colors on background or color are dangerous — they're paint commands that no stylesheet can dethrone.
A side benefit
Once the hues were CSS variables instead of inline backgrounds, I could expose them to other parts of the component for free. The cell's box-shadow glow uses the same variables. The current-cell pulse animation uses --hue-glow. The ghost-number outline uses color-mix(in srgb, var(--hue-mid) 25%, transparent). Adding a new effect doesn't require touching React state — just touching the stylesheet.
Try It
Live in production — both modes work:
- See the caller light mode: bingwow.com/caller (toggle theme in the navbar)
- Create a multiplayer card: bingwow.com/create
- Browse cards: bingwow.com/cards
- Free, no signup, no premium tier: bingwow.com
If you've hit the same wall with theming a React component, I'd love to hear the route you took. Drop it in the comments.
Top comments (1)
Hello how are you doing ?