4 min read
Color Spaces
You inverted the palette for dark mode. The gray text that was #63635e on white became #969696 on dark. It looked fine in the checker. On a real display the secondary text glows slightly on black, and border grays disappear into the background.
The Principle
Hex and hsl() are not perceptually uniform. A gray that reads mid-tone on white can look bright on a dark background. OKLCH uses perceived lightness instead of mathematical lightness, so the same L value holds across light and dark themes.
color-mix() blends two colors in a chosen space. Derive muted text, borders, and hover states from one base token instead of adding a new hex for every variant.
The Rule I Follow
I define text colors in oklch() with the same hue and a low chroma for neutral grays. For dark mode I retune L values instead of inverting hex. I use color-mix() for borders, underlines, and hover fills so every derived value follows when the base token changes.
Try It Yourself
Drag Hue and watch both panels update at the same angle. The gap between them is the difference between color spaces.
OKLCH keeps chroma and lightness steadier as hue changes. HSL oversaturates yellows and cyans while other hues look duller by comparison.
The Mistake I'd Avoid
Defining a new hex value for every opacity variant: --color-border, --color-border-strong, --color-border-subtle. Three tokens where color-mix() gives you all three from one.
Remember This
Build color systems in a perceptual space. Derive variants with color-mix(), not new hex values.
:root {
--color-text-primary: oklch(0.21 0.005 80);
--color-text-secondary: oklch(0.43 0.01 80); /* tune L for light backgrounds */
}
:root[data-theme="dark"] {
--color-text-primary: oklch(0.92 0.005 80);
--color-text-secondary: oklch(0.65 0.01 80); /* retune L, keep hue + chroma */
}
a {
text-decoration-color: color-mix(
in srgb,
currentColor 40%,
transparent
); /* one token, many opacities */
}:root {
--color-text-primary: oklch(0.21 0.005 80);
--color-text-secondary: oklch(0.43 0.01 80); /* tune L for light backgrounds */
}
:root[data-theme="dark"] {
--color-text-primary: oklch(0.92 0.005 80);
--color-text-secondary: oklch(0.65 0.01 80); /* retune L, keep hue + chroma */
}
a {
text-decoration-color: color-mix(
in srgb,
currentColor 40%,
transparent
); /* one token, many opacities */
}