Here is HSL’s dirty secret: hsl(60 100% 50%) yellow and hsl(240 100% 50%) blue claim the same lightness—and the yellow is blindingly brighter. HSL’s numbers describe the math of RGB, not the physics of your eye. Which is why palettes generated by rotating HSL hues always look slightly radioactive.

OKLCH is perceptually uniform: equal numbers mean equal appearance. Two colors with the same L are genuinely equally bright, regardless of hue. That single property changes how design systems generate color.

Palettes become arithmetic

:root {
  /* one brand hue, honest lightness ramp */
  --brand-100: oklch(95% 0.03 285);
  --brand-500: oklch(62% 0.19 285);
  --brand-900: oklch(32% 0.12 285);

  /* accessible pairs, computable */
  --on-brand: oklch(98% 0.01 285);
}

Because L is honest, contrast becomes predictable: keep roughly 40–50 points of L between text and background and you clear WCAG in every hue. Need a hover state? Subtract 6% L. Need the same intensity in ten hues? Fix L and C, rotate H—and every step actually matches, which HSL never delivered.

“A color system where the numbers lie will always need a human to check its work.”

The practical bits

Support is universal in modern browsers, and OKLCH can express the whole P3 gamut—colors your screen can show but hex cannot name. Watch chroma at the extremes: not every L/C/H combination exists physically, so out-of-gamut values get clamped; keep C modest for very light and very dark steps. Migrate by converting your existing hex tokens in place—same colors, honest coordinates—then let the arithmetic take over from there.