OKLCh explained, and why HSL keeps letting you down

HSL is easy to teach and mostly fine for nudging a colour. It falls apart the moment you need two colours to look equally bright.

The problem with HSL lightness

Take hsl(60 100% 50%) and hsl(240 100% 50%). Both claim 50% lightness. The first is a vivid yellow, the second a deep blue. They look nothing like equally bright — the yellow is dazzling and the blue is almost dark.

This is not a bug. HSL is a straightforward geometric reshaping of encoded RGB values. Its “lightness” is the midpoint between the largest and smallest channel, which has no connection to how bright a colour appears. Human vision is far more sensitive to green than to blue, and HSL knows nothing about that.

The practical consequence: any scale you build by holding HSL lightness and rotating hue will have some steps that jump out and others that recede.

What OKLCh does differently

OKLCh is built on OKLab, a space designed so that equal numeric distances correspond to roughly equal perceived differences. Its three coordinates are:

  • L, lightness — 0 to 1 (written as 0% to 100% in CSS), matching perceived brightness
  • C, chroma — 0 upward, how colourful. Unlike HSL saturation this is not a percentage of some maximum; it is an absolute amount, and the reachable maximum depends on the hue and lightness
  • H, hue — 0 to 360 degrees

The payoff is immediate. oklch(70% 0.15 60) and oklch(70% 0.15 250) genuinely do look equally bright.

Chroma is not saturation

This trips people up. In HSL, 100% saturation is always valid. In OKLCh, whether a given chroma is reachable depends on the hue and lightness, because the sRGB gamut is a lumpy shape rather than a cylinder.

Yellow can reach high chroma at high lightness. Blue cannot — a highly saturated blue is inherently dark. So oklch(90% 0.3 250) describes a bright, intensely saturated blue that simply does not exist on your monitor.

That is not a flaw. It means OKLCh can describe colours wider than sRGB, which is what you want for Display P3 output. It also means something has to happen when you need an sRGB value.

Gamut mapping, and why clipping is bad

The naive fix is to clamp each RGB channel to 0–1. This works but shifts hue: clip a saturated purple and you can watch it slide toward blue.

The better approach, and what CSS Color 4 specifies, is to hold lightness and hue fixed and reduce chroma until the colour fits. You lose some vividness and keep the colour’s identity. Every colour on this site is mapped that way, which is why an out-of-gamut input still shows a sensible hex value.

Building a tonal scale

This is where OKLCh earns its place in a design system. To build a Tailwind-style 50–950 ramp, hold hue, walk lightness, and ease chroma down at the extremes:

:root {
  --brand-50:  oklch(97.7% 0.013 250);
  --brand-200: oklch(89.5% 0.055 250);
  --brand-400: oklch(73.5% 0.13 250);
  --brand-500: oklch(63.7% 0.16 250);
  --brand-700: oklch(47%   0.14 250);
  --brand-900: oklch(34%   0.09 250);
}

Because lightness is perceptual, the steps look evenly spaced. Try the same thing in HSL and the middle of the ramp bunches up.

The chroma easing matters too. Holding chroma constant while lightness approaches the extremes produces very light tints that look oddly intense and very dark shades that look muddy, so reducing it toward both ends gives a more natural ramp.

Interpolation

OKLab is also the right space for mixing. The notorious case is blending yellow into blue: do it in encoded sRGB and the midpoint is a muddy grey; do it in OKLab and you pass through a plausible green.

CSS lets you ask for this directly:

background: linear-gradient(in oklab, yellow, blue);

For hue-based interpolation, use in oklch and CSS will take the shorter way around the hue circle by default.

When to still use HSL

HSL is not useless. If you want to nudge a colour slightly lighter and you do not care about cross-hue consistency, it is fine and everyone can read it. It is also a reasonable model for a colour picker’s controls, which is why the square-and-hue-slider arrangement persists.

Use OKLCh when consistency matters: scales, themes, generated palettes, anything where two colours need to look related. See any colour’s OKLCh values or convert hex to OKLCh directly.

Questions

Is OKLCh supported in browsers?

Yes. The oklch() function is supported across all current major browsers. If you need to support much older versions, provide a hex fallback before the oklch declaration and the older browser will ignore the line it does not understand.

What is the difference between OKLab and OKLCh?

They describe the same space in different coordinates. OKLab is rectangular, with lightness plus two opponent axes. OKLCh is the polar form of the same thing: lightness, chroma and hue. OKLCh is easier to reason about by hand because hue is a single number you can rotate.

Why does my chroma value get clamped?

OKLCh can describe colours that no sRGB display can show. When the colour has to be rendered in sRGB, chroma is reduced while lightness and hue are held, which keeps the colour recognisable. Clipping each RGB channel instead would shift the hue noticeably.

More guides

  • sRGB vs linear colour in shadersWhat display (sRGB) and linear colour numbers mean, which to use in shaders and lighting, and how Unity, Unreal and the web treat the same hue.
  • Choosing accessible text coloursHow WCAG 2 contrast ratios work, where they go wrong, what APCA does differently, and a practical method for picking text colours that pass and look right.
  • Hex colour codes explainedWhat the six digits in a hex colour actually mean, how the shorthand and alpha forms work, and how to read or write one by hand.