Back to the blog
Design Systemsยท Feb 17, 2026ยท 5 min read

Why we moved every color to OKLCH

Perceptually uniform color, predictable dark mode, and gradients that don't go grey in the middle. A practical migration guide.

#css#design-systems#color#accessibility

This site's entire color system - the one you're looking at right now - is defined in OKLCH instead of hex or HSL. That wasn't a stylistic choice; it solved three specific, recurring bugs that kept showing up across every product in our suite.

The problem with HSL that nobody mentions

HSL's lightness channel doesn't match human perception. `hsl(60, 100%, 50%)` (yellow) and `hsl(240, 100%, 50%)` (blue) have the same "L" value on paper but look nowhere near equally bright to an actual human eye. That mismatch is why HSL-based palettes so often need manual per-hue tweaking to feel balanced - you're fighting the color space, not your design instincts.

What OKLCH fixes

OKLCH's lightness channel is built from a perceptual model, so two colors with the same L value actually look equally light to a human, regardless of hue. That property alone fixes a surprising number of downstream problems.

  • Generating a dark mode palette became a mechanical transform - invert lightness, keep chroma and hue roughly stable - instead of a manual re-design of every token.
  • Gradients between two colors of the same lightness no longer dip through a muddy grey in the middle, because the interpolation path stays perceptually even.
  • Accessibility contrast checks got more predictable, since lightness differences map much more directly to perceived contrast than they do in HSL or RGB.
:root {
  --primary: oklch(0.55 0.2 258);      /* L C H */
  --primary-glow: oklch(0.68 0.18 258); /* same hue, lighter */
}

The migration in practice

We didn't rewrite every color by hand. We picked our existing brand hex values, converted them to OKLCH with a script, and then manually tuned exactly two things: the lightness curve for our dark-mode palette, and the chroma of a handful of semantic colors (success, warning, destructive) that looked oversaturated once they were perceptually "equal" to the rest of the palette. Everything else carried over mechanically.

The catch

Browser support for `oklch()` in CSS is solid in every evergreen browser at this point, but if you need to support genuinely old browsers, you'll want a fallback chain (`color: #4a5fff; color: oklch(0.55 0.2 258);` - CSS ignores values it can't parse, so this degrades safely). For us, that wasn't a constraint worth designing around.

Written by Appesto Engineering.