guide · modern CSS color

OKLCH, explained for people who ship

OKLCH is the color space CSS should have started with: three numbers you can reason about, a lightness axis that matches your eyes, and enough range to cover wide-gamut screens.

oklch ramp from #6366f1 — even perceived steps

oklch ramp from #eab308 — same evenness at a very different hue

The three axes

  • L — lightness, 0 to 1. 0 is black, 1 is white, and 0.5 genuinely looks mid-grey regardless of hue.
  • C — chroma, 0 upward. How colorful the color is. Unlike HSL saturation it is absolute, not relative to the maximum for that hue.
  • H — hue, 0 to 360 degrees. Roughly 30 is orange, 145 green, 265 blue, 350 pink.

The problem OKLCH solves

In hsl(), yellow at 50% lightness looks nearly white while blue at 50% looks almost black. Any scale generated by stepping lightness therefore looks lumpy, and any automated contrast decision built on it is guesswork. Because OKLCH lightness is perceptual, a fixed set of lightness targets produces a ramp that reads evenly at every hue — exactly what the two ramps above demonstrate.

Gamut: sRGB, Display-P3, Rec.2020

OKLCH can express colors no sRGB screen can show. That is a feature: you can author one punchy accent and let the browser clamp it for narrower displays. Keep chroma near or below 0.15 if you want a color to be identical everywhere, and check the gamut badge in the converter when you push higher.

Token patterns worth copying

Derive an entire interactive state set from one stored color:

:root {
  --brand: oklch(0.585 0.204 277);
  --brand-hover:  oklch(from var(--brand) calc(l - 0.05) c h);
  --brand-active: oklch(from var(--brand) calc(l - 0.10) c h);
  --brand-subtle: oklch(from var(--brand) 0.96 calc(c * 0.2) h);
  --on-brand: contrast-color(var(--brand));
}

Add a light/dark surface pair without a media query using light-dark():

:root { color-scheme: light dark; }
body {
  background: light-dark(oklch(0.99 0.003 265), oklch(0.17 0.012 265));
  color: light-dark(oklch(0.21 0.02 265), oklch(0.96 0.008 265));
}

Blending that does not go muddy

color-mix(in oklch, var(--a), var(--b)) passes through a sensible midpoint, where the same mix in sRGB dips into grey. The same applies to gradients: add in oklch to a linear-gradient() and the dead zone disappears. Compare both live in the gradient generator.

Migrating an existing palette

  1. Convert your brand color with the HEX to OKLCH tool.
  2. Generate primary, neutral and status ramps plus semantic tokens in the design system generator.
  3. Export CSS variables, a Tailwind theme block, or Figma Variables JSON.
  4. Verify text pairs in the contrast checker and repair failures perceptually rather than by eye.

Frequently asked questions

What does OKLCH stand for?
It is the cylindrical form of the Oklab color space: Lightness, Chroma and Hue. Oklab was designed so that equal numeric distances correspond to roughly equal perceived differences.
Is OKLCH supported in browsers?
Yes. oklch() and oklab() are supported across all current major browsers, and relative color syntax plus contrast-color() reached baseline availability in 2026.
Is OKLCH better than HSL?
For anything computed, yes. HSL lightness does not match perception, so ramps and hover states drift. OKLCH keeps lightness honest, which makes generated scales and contrast decisions reliable.
What chroma values should I use?
Roughly 0 to 0.05 for neutrals, 0.05 to 0.12 for muted UI colors, and 0.15 to 0.25 for brand accents. Above about 0.25 many hues fall outside sRGB.
How do I convert my existing palette?
Paste each HEX into the converter, or drop your brand color into the design system generator to produce full OKLCH ramps and semantic tokens in one step.