Dark Mode Color System with color-mix()
Use color-mix() to derive both light and dark theme palettes from one brand token. Switch themes by changing the 'base' rather than maintaining two parallel scales.
Detailed Explanation
One Brand, Two Themes
A common pain point in design systems is maintaining two parallel
palettes — one for light mode and one for dark mode. color-mix() lets
you derive both from the same brand color by changing the base the
mixer pulls toward:
:root {
--brand: #2563eb;
--bg: white;
--fg: #0a0a0a;
}
[data-theme="dark"] {
--bg: #0a0a0a;
--fg: #fafafa;
}
/* Surfaces derived from --brand and --bg */
.surface-1 { background: color-mix(in oklch, var(--brand) 4%, var(--bg)); }
.surface-2 { background: color-mix(in oklch, var(--brand) 8%, var(--bg)); }
.surface-3 { background: color-mix(in oklch, var(--brand) 12%, var(--bg)); }
In light mode, --bg is white, so each surface is a faintly
brand-tinted near-white. In dark mode, --bg is near-black, so the same
formula produces brand-tinted near-blacks — automatically.
Pressed/hover states from --fg
.button:hover {
background: color-mix(in oklch, var(--brand), var(--fg) 12%);
}
This darkens in light mode (foreground is dark) and lightens in dark mode (foreground is light). One rule, both themes.
Caveats
- Always test the resulting contrast in both themes — chroma stays similar but lightness flips. Use the accessibility color checker to validate AA/AAA pairs.
- Some hue families (yellows, cyans) need a slightly different mix percentage between themes to feel balanced; treat the percentages as starting points, not gospel.
Use Case
Cut your design tokens roughly in half. Useful for product teams that ship a single component library across multiple themes (light/dark/high-contrast) without doubling the maintenance burden.