Why your design system needs OKLCH (and what to actually use it for)
If you’ve ever tried to make a “blue 500” and a “green 500” that look like
the same level of darkness, you’ve run into HSL’s biggest problem: its
“lightness” axis lies. hsl(220 80% 50%) and hsl(120 80% 50%) have the
same L value. They look nothing alike.
OKLCH fixes this. The same L in OKLCH actually gives you the same
perceived darkness across hues. That’s the whole pitch, and it unlocks
four things you couldn’t easily do before.
1. Tonal scales that are actually tonal
In Tailwind / Material / IBM Carbon, “blue 500” and “red 500” are picked by hand to look like the same darkness. That hand-tuning is a tax on every design system that wants more than the standard 11 steps.
In OKLCH, you generate them:
:root {
--blue-50: oklch(96% 0.04 260);
--blue-100: oklch(92% 0.07 260);
--blue-200: oklch(85% 0.12 260);
--blue-300: oklch(75% 0.18 260);
--blue-400: oklch(65% 0.20 260);
--blue-500: oklch(55% 0.22 260);
--blue-600: oklch(45% 0.20 260);
--blue-700: oklch(35% 0.18 260);
--blue-800: oklch(25% 0.14 260);
--blue-900: oklch(15% 0.08 260);
}
Change 260 to 25 and you get a red scale where the lightness levels
match. Change to 145 and you get a green scale that matches both. The
human-perceived contrast between any two adjacent steps is consistent
across hues.
2. Accessible text-on-color, predictably
WCAG contrast comes from luminance. OKLCH’s L is approximately
perceptual lightness, which correlates better with luminance than HSL’s
L does. In practice this means: L < 50% reliably needs white text;
L > 65% reliably needs dark text. The 50–65% band is where you
actually have to check.
Compare HSL: hsl(60 100% 50%) is yellow with L=50% but luminance is
~0.93 — definitely needs dark text. HSL’s lightness misled you.
This single rule lets you generate --text-on-blue-500, --text-on-red-500,
etc. mechanically rather than picking each by hand.
3. Real animation between colors
CSS transitions on color interpolate in sRGB by default — the path between red and green passes through ugly grey-brown territory. OKLCH interpolation goes through a hue arc that stays vivid:
.button {
background: oklch(55% 0.22 260);
transition: background 200ms;
}
.button:hover {
background: oklch(55% 0.22 25);
transition: background 200ms linear;
/* Browsers that support it: */
transition-behavior: allow-discrete;
}
For named-color → named-color crossfades, OKLCH path looks dramatically better than sRGB. As of 2026 this is shipping in stable Chrome / Firefox / Safari behind the standard color interpolation hint:
.fade {
--from: oklch(55% 0.22 260);
--to: oklch(55% 0.22 25);
background: color-mix(in oklch, var(--from), var(--to) 50%);
}
4. Sane high-contrast mode and dark themes
Generating a dark theme from a light theme is mostly “invert lightness.” In OKLCH:
:root[data-theme="dark"] {
--bg: oklch(15% 0.01 250); /* was 98% */
--fg: oklch(95% 0.01 250); /* was 15% */
--accent: oklch(70% 0.18 260); /* was 55% */
}
That last line is the trick: when you flip light→dark, the accent has to
get lighter, not darker, because it now sits on a dark background. OKLCH
makes the L/C/H axes orthogonal enough that you can adjust just L
without the hue shifting.
Try it the same way in HSL and you’ll find yourself hand-tuning every swatch — the perceived hue shifts as L changes.
What about wide-gamut?
OKLCH was designed for display-p3 and wider gamuts. Most browsers can
display oklch values that are out-of-gamut for sRGB and clamp gracefully
on sRGB displays. The practical takeaway: feel free to push C (chroma)
beyond 0.30 — modern devices will show vivider colors, older ones see a
clamped fallback.
Caveats
- Browser support. OKLCH shipped in all modern browsers in 2023.
Safari 15 and earlier need a fallback. Use
@supports (color: oklch(0% 0 0))or a build-time fallback compiler. - Tooling. Most design tools still output hex/HSL. Expect a year or two of “I picked it in Figma, what’s the OKLCH” friction. The color converter handles that round-trip.
- Gotcha: the
H(hue) axis in OKLCH is not the same as HSL hue. HSL red is 0°, OKLCH red is ~25°. Mental conversion is non-trivial — I keep a cheat sheet:- red ≈ 25
- orange ≈ 50
- yellow ≈ 90
- green ≈ 145
- cyan ≈ 195
- blue ≈ 260
- violet ≈ 295
- magenta ≈ 330
TL;DR
OKLCH gives you a perceptually-uniform color space directly in CSS. The practical wins for design systems:
- Generate tonal scales programmatically — same
Lacross hues looks like same darkness. - Predict text-on-color from
Lalone —<50%= white text,>65%= dark text. - Animate colors smoothly through
color-mix(in oklch, ...). - Build a dark theme by inverting
Linstead of hand-tuning each swatch.
Plug any color into the color tool — it shows the OKLCH form alongside hex/RGB/HSL so you can start training your eye.
Related across the network
- hex.tooljo.com/contrast — WCAG AA/AAA checker. Pair it with OKLCH lightness rules above.
- hex.tooljo.com/blog/tailwind-v4-oklch — what changed when Tailwind v4 made OKLCH the default.