The problem with "two palettes"
Many teams build their light theme first, ship it, and then bolt on dark mode later as a completely separate set of colors. The result is predictable: the two themes drift apart. A new component looks great in light mode and unreadable in dark, because nobody remembered to define the dark variant.
The fix is architectural. You should never have a "light palette" and a "dark palette." You should have one set of semantic tokens with two value sets behind them.
Semantic tokens are theme-agnostic
When your components reference intent (--color-surface, --color-text-primary) instead of appearance (--white, --black), switching themes is just swapping the values:
:root {
--color-bg: #F8FAFC;
--color-surface: #FFFFFF;
--color-text-primary: #0F172A;
--color-border: #E2E8F0;
}
[data-theme="dark"] {
--color-bg: #020617;
--color-surface: #0F172A;
--color-text-primary: #E5E7EB;
--color-border: rgba(255,255,255,0.08);
}
The components never change. Only the values under the same names do. This is why a well-tokenized app can switch themes instantly with zero rebuild.
Dark mode is not "inverted" light mode
A common shortcut is to mathematically invert lightness. It never looks right. Three rules matter:
- Don't use pure black. A tinted near-black like
#020617or#0F172Areduces eye strain and halation compared to#000000. - Show elevation with lightness, not shadow. Shadows are nearly invisible on dark backgrounds. Make elevated surfaces (cards, modals) lighter than the page behind them.
- Desaturate your accents. A color that looks vivid on white "vibrates" on black. Lower saturation 10–20% and often raise lightness to keep contrast.
We go deeper on the perceptual side in Designing Dark Mode: It's Not Just Inverting Colors.
Keep both themes accessible
Contrast requirements (4.5:1 for body text, 3:1 for large text) apply to both themes. The trap is muted text: a secondary gray that passes in light mode often fails on a dark surface. Verify each semantic text token against each background token in both themes. Our palette detail pages show a real-time contrast score for exactly this reason—test the combination, not the swatch in isolation.
A repeatable workflow
- Define semantic tokens once (bg, surface, border, text-primary, text-muted, primary, danger, success).
- Fill in the light value set, then the dark value set, for every token.
- Check contrast for each text-on-surface pairing in both themes.
- Build components against semantic names only—never hardcode a hex.
- Add a new component? You're forced to define both theme values up front, so drift can't happen.
If you want a head start, our dark mode UI color palettes are tuned for comfortable contrast and pair naturally with light counterparts. Combine that with a semantic token system and you'll maintain one coherent system instead of two diverging ones.