iKit
Comparison · 9 min read ·

HSL vs OKLCH: Why Design Systems Are Switching (2026)

HSL vs OKLCH comes down to one thing: HSL's lightness shifts across hues while OKLCH stays perceptually even. Here's why design systems switch in 2026.

HSL vs OKLCH: Why Design Systems Are Switching (2026)

HSL vs OKLCH: Why Design Systems Are Switching

If you have ever set a row of status colors to the same HSL lightness and watched the yellow glow while the blue sank, you have met the flaw OKLCH was built to fix. HSL is a cylinder wrapped around sRGB; its lightness number is geometry, not perception. OKLCH measures lightness the way your eye does. For design systems built on shade ramps and contrast rules, that difference decides which one you should reach for.

TL;DR

  • HSL lightness is geometric — the same L looks brighter for yellow than for blue.
  • OKLCH lightness is perceptual, so one L value reads evenly across all hues.
  • Changing an HSL hue can silently drop contrast; OKLCH keeps it stable.
  • Design systems switch because OKLCH ramps and themes stay balanced automatically.
  • Both work in every current browser; OKLCH also reaches wider P3 colors.

What is the difference between HSL and OKLCH?

Both functions describe a color with three numbers you can reason about, and both put hue on a wheel. The gap is what the lightness number actually means and which color space sits underneath.

HSL: a cylinder wrapped around sRGB

HSL takes the sRGB cube and bends it into a cylinder: hue is the angle, saturation is the distance from the center, lightness is the height. It was designed in the 1970s to be friendlier than raw RGB, and it succeeded. But the cylinder is a convenience, not a measurement. Every hue is stretched to span the same 0–100% saturation and lightness range, even though real displays and human eyes hit their maximum saturation at wildly different points for red, green, and blue. HSL hides that by deforming the space.

OKLCH: perceptual lightness in polar form

OKLCH is the cylindrical form of the Oklab color space, published by Björn Ottosson in December 2020. Oklab was tuned against perceptual datasets so that its L axis tracks perceived lightness, and its two Cartesian axes were designed so blending stays even. OKLCH keeps that L axis and swaps the two Cartesian coordinates for a chroma distance and a hue angle — the same move that turns RGB into HSL, but on a foundation calibrated to human vision. It landed in CSS Color Module Level 4 alongside oklab(), lab(), and lch().

Reading the same blue in both

Here is one blue written two ways. Both render nearly the same pixel, but only one of the two number sets tells you anything reliable when you start editing it:

.a { color: hsl(210 60% 64%); }
.b { color: oklch(0.7 0.1 250); }

In the OKLCH version, 0.7 is a promise: raise it and the color gets perceptibly lighter by a predictable amount, whatever the hue. In the HSL version, 64% only means "64% of the way up this particular hue's deformed cylinder."

Why is HSL bad for design systems?

A design system lives or dies by consistency across many colors. That is exactly where HSL's convenient distortion starts to cost you.

HSL lightness is not perceptually uniform

Set a vivid yellow and a vivid blue to identical HSL lightness — hsl(90 100% 50%) and hsl(250 100% 50%) — and the yellow will look dramatically brighter than the blue. Björn Ottosson's own measurements show HSV (HSL's close cousin) as the worst performer of every color model he tested for predicting lightness, with an error an order of magnitude larger than Oklab's. So when you build a token system on HSL, your "same lightness" tier isn't the same brightness at all. A neutral-500 and a brand-500 that share an L value can read as different weights on screen, and your careful hierarchy wobbles.

Changing hue silently changes contrast

This is the one that produces real bugs. Say you generate an error variant of your accent color by rotating the hue toward red. In HSL, that rotation also changes perceived lightness, so text that passed contrast on the accent background can quietly fail on the error background — same L number, different real brightness. The Evil Martians team documented exactly this failure, noting that in HSL, hue changes can lead to accessibility issues from low contrast. OKLCH holds lightness constant when you move hue, so contrast stays where you put it.

The Sass darken() surprise

If you used Sass, you probably hit this: darken($color, 10%) gives a satisfying result for one brand color and an ugly, muddy, or barely-changed result for another. That is HSL lightness math applied to hues that don't share a perceptual scale. Whole teams eventually asked the community to stop using HSL for color-system palette generation for this reason. With OKLCH, calc(l - 0.1) subtracts the same perceived darkness from any hue, so a single darkening rule behaves the same across your entire palette.

HSL vs OKLCH: a side-by-side comparison

Neither function is universally "better" — they optimize for different jobs. Here is the honest split.

The comparison table

Property HSL OKLCH
Underlying space sRGB cylinder Oklab (perceptual)
Lightness meaning Geometric Perceptual
Even shade ramps No Yes
Hue-safe contrast No Yes
Wide-gamut P3 No Yes

A second, narrower table for the practical trade-offs:

Question Answer
Familiar to most devs HSL
Better for tokens & themes OKLCH
Browser support in 2026 Both

Building an even tint-and-shade ramp

This is the payoff most teams switch for. Hold chroma and hue, step lightness evenly, and OKLCH hands you a balanced scale:

:root {
  --brand-100: oklch(0.95 0.03 250);
  --brand-300: oklch(0.80 0.08 250);
  --brand-500: oklch(0.62 0.14 250);
  --brand-700: oklch(0.45 0.12 250);
  --brand-900: oklch(0.28 0.08 250);
}

Each step drops L by a consistent perceptual amount, so the ramp looks as even as it reads. Try the same trick with HSL lightness stops and the middle of the ramp will bunch up or wash out depending on the hue. A generator like the iKit Color Palette Generator leans on this uniformity to produce scales that stay balanced across hues.

When HSL is still the right call

OKLCH is not a mandate. HSL is genuinely fine when:

  • You need a quick one-off tweak and think in the familiar hue/saturation/lightness sliders.
  • You are exploring a hue by hand and don't care about cross-hue consistency.
  • You are shipping a tiny stylesheet with no shade ramps or generated themes.

For fixed brand values, plain HEX is still perfectly good too — you can read any HEX code as HSL, OKLCH, or RGB in the iKit Color Picker & Converter when you need to check what a value maps to.

How to convert HSL to OKLCH in CSS

There is no tidy algebraic formula between them, because HSL is glued to sRGB and OKLCH is a perceptual space — the conversion runs through Oklab's matrices. In practice you never do it by hand.

The mechanical conversion

Paste the HSL or HEX value into a converter, or open your browser devtools color picker, which shows the OKLCH equivalent next to the HEX. Because both functions are native CSS, you can drop the result straight in and layer a fallback for old browsers:

.btn {
  background: hsl(210 60% 64%);      /* fallback */
  background: oklch(0.7 0.1 250);    /* modern  */
}

A browser that doesn't understand the second line keeps the first — no @supports gymnastics required, though you can add one for certainty.

Theming with relative colors

Once your source colors are in OKLCH, CSS relative color syntax turns a single accent into a whole interaction set. The predictable lightness is what makes one rule safe to reuse:

.btn {
  --accent: oklch(0.62 0.14 250);
  background: var(--accent);
}
.btn:hover {
  /* 8% lighter, same hue and chroma */
  background: oklch(from var(--accent) calc(l + 0.08) c h);
}
.btn.is-error {
  /* swap only the hue; lightness stays put */
  background: oklch(from var(--accent) l c 25);
}

That is-error line is the whole argument in miniature: you change the hue and your contrast survives, because L never moved. The same math powers even hue-to-hue gradients when you interpolate in oklch.

P3 colors HSL can't reach

There is a reach advantage too. HEX, RGB, and HSL can only address colors inside sRGB. OKLCH can point past it into Display P3, which covers meaningfully more saturated reds and greens on the P3-capable screens most laptops and phones now ship. A value like oklch(0.65 0.28 25) is a red more vivid than #ff0000; on an sRGB screen the browser simply gamut-maps it back to the nearest color it can show. Chroma has no hard ceiling but stays under about 0.37 in practice, with the real maximum depending on hue and gamut.

References

Related on iKit

Related posts