Typable.

OKLCH: Why You Should Ditch RGB and HSL

The problem you probably didn’t know you had

Have you ever built a color palette in HSL, picked identical lightness values for every hue, and then noticed that yellow looked way brighter than blue — even though they were technically “equal”?

It’s not a browser bug. It’s a structural limitation in how HSL and RGB represent color.

HSL was a big step forward from RGB: finally we could think in terms of hue, saturation, and lightness instead of mixing red, green, and blue channels. But HSL has a fundamental flaw — its L doesn’t match how our eyes actually perceive brightness. Two colors with L: 50% can look completely different in perceived brightness.

That’s why OKLCH exists: a color space designed around human perception, not around how monitors work.


RGB, HSL, and OKLCH: a quick comparison

Before diving into the details of OKLCH, it’s worth understanding where it sits relative to the color spaces you already know.

RGB

RGB describes a color as a combination of red, green, and blue — the three channels of a monitor’s pixels. It’s the native language of screens, but it’s completely unintuitive for us:

color: rgb(41, 182, 246); /* What color is this? Impossible to tell by eye */

There’s no way to visually infer hue or lightness from these three numbers. RGB is great for machines, terrible for humans.

HSL

HSL was created specifically to solve this: it expresses color as Hue (0–360°), Saturation (0–100%), and Lightness (0–100%).

color: hsl(199, 89%, 56%); /* Much better: light blue */

It’s far more readable, but it suffers from non-uniform lightness. Look at this example:

/* Same L value: 60% — but NOT perceptually equal */
.yellow { color: hsl(60,  90%, 60%); }
.blue   { color: hsl(220, 90%, 60%); }
.green  { color: hsl(120, 90%, 60%); }

The yellow will look much brighter than the blue. HSL’s L is a (well-intentioned) lie.

OKLCH

OKLCH also uses three axes, but they’re built on perceptual models of human vision:

color: oklch(0.72 0.18 199); /* Light blue, with genuinely uniform lightness */

The key difference: if two OKLCH colors share the same L value, they will actually appear at the same perceived brightness. That changes everything when it comes to building palettes.

Feature RGB HSL OKLCH
Human readability
Perceptually uniform lightness
Access to the full P3 gamut
Suited for design systems ⚠️

The structure of oklch() in detail

color: oklch(L C H);
/* or with alpha */
color: oklch(L C H / alpha);

Lightness (L)

Ranges from 0 (black) to 1 (white). Unlike HSL, this value is perceptually linear: 0.5 really is halfway between black and white to our eyes.

oklch(0.2 0.15 250)  /* Very dark blue */
oklch(0.5 0.15 250)  /* Medium blue */
oklch(0.8 0.15 250)  /* Light blue */

Chroma (C)

Represents the intensity or saturation of the color. 0 is neutral gray, higher values produce more vivid colors. The maximum depends on the hue and the device’s color space — on P3 displays it can reach roughly 0.37.

oklch(0.6 0.0  250)  /* Neutral gray */
oklch(0.6 0.1  250)  /* Desaturated blue */
oklch(0.6 0.25 250)  /* Vivid blue */

Hue (H)

The hue in degrees, just like in HSL. But be careful: the arrangement isn’t identical. In OKLCH, roughly:

oklch(0.65 0.2 30)   /* Orange */
oklch(0.65 0.2 145)  /* Green */
oklch(0.65 0.2 250)  /* Blue */
oklch(0.65 0.2 320)  /* Pink/magenta */

The main advantage: building consistent palettes

This is the reason OKLCH is taking over modern CSS. Building a harmonious palette becomes trivial.

Monochromatic palette

Keep C and H fixed, vary only L:

:root {
  --brand-900: oklch(0.20 0.18 250);
  --brand-700: oklch(0.35 0.18 250);
  --brand-500: oklch(0.50 0.18 250);
  --brand-300: oklch(0.70 0.18 250);
  --brand-100: oklch(0.90 0.18 250);
}

Result: five variants of the same blue, perceptually balanced. In HSL you’d have had to manually tweak each value because real lightness isn’t uniform.

Complementary palette

Same L and C, rotate H by 180°:

:root {
  --primary:       oklch(0.55 0.22 250); /* Blue */
  --complementary: oklch(0.55 0.22 70);  /* Orange/yellow */
}

Both colors will have the same perceived lightness and the same visual intensity — something very hard to achieve with HSL.

Analogous palette

Shift H in small increments:

:root {
  --color-1: oklch(0.60 0.20 220); /* Cyan */
  --color-2: oklch(0.60 0.20 250); /* Blue */
  --color-3: oklch(0.60 0.20 280); /* Indigo */
}

Palette in CSS with custom properties: full example

:root {
  /* Primary color */
  --hue-primary: 250;
  --chroma-primary: 0.20;

  --primary-100: oklch(0.95 calc(var(--chroma-primary) * 0.3) var(--hue-primary));
  --primary-300: oklch(0.80 calc(var(--chroma-primary) * 0.6) var(--hue-primary));
  --primary-500: oklch(0.60 var(--chroma-primary) var(--hue-primary));
  --primary-700: oklch(0.40 var(--chroma-primary) var(--hue-primary));
  --primary-900: oklch(0.25 calc(var(--chroma-primary) * 0.8) var(--hue-primary));

  /* Just change --hue-primary to get a whole new theme */
}

Change a single value — --hue-primary — and the entire palette regenerates in a perceptually balanced way. Try doing that with HSL.


OKLCH for accessibility and contrast

OKLCH’s perceptually uniform lightness has a direct impact on accessibility. When building text/background combinations, you can use the difference in L as a reliable estimate of perceived contrast.

A practical rule of thumb:

/* High contrast: L difference ≥ 0.5 */
.text-on-dark-background {
  background: oklch(0.20 0.05 250); /* L = 0.20 */
  color:      oklch(0.95 0.02 250); /* L = 0.95 — difference: 0.75 ✅ */
}

/* Insufficient contrast: L difference < 0.3 */
.warning {
  background: oklch(0.50 0.15 250); /* L = 0.50 */
  color:      oklch(0.65 0.15 250); /* L = 0.65 — difference: 0.15 ⚠️ */
}

This doesn’t replace formal WCAG verification, but it gives a far more reliable gut check than HSL for eyeballing contrast while designing.


Current browser support

OKLCH is natively supported in all modern browsers, no prefixes required:

Current global coverage exceeds 93% of users. For legacy browsers still in circulation, the recommended strategy is an explicit fallback:

.element {
  /* Fallback for legacy browsers */
  color: hsl(220, 70%, 55%);

  /* OKLCH for modern browsers */
  color: oklch(0.55 0.18 250);
}

Or with @supports for more complex logic:

@supports (color: oklch(0 0 0)) {
  :root {
    --primary: oklch(0.55 0.18 250);
  }
}

@supports not (color: oklch(0 0 0)) {
  :root {
    --primary: hsl(220, 70%, 55%);
  }
}

If you use PostCSS, the postcss-oklab-function plugin automatically converts oklch() into compatible values at build time — zero compromises in the source code, maximum compatibility in production.


Tools and resources

Editors and pickers

Libraries and integrations

Further reading


Conclusion

OKLCH isn’t just a more modern syntax for writing colors in CSS. It’s a paradigm shift in how you think about color when designing an interface.

With RGB you reasoned in physical channels. With HSL you reasoned in hue and lightness — but that lightness was unreliable. With OKLCH you finally reason in terms of how color is perceived: consistent palettes without manual tweaking, predictable contrast, design systems that scale by changing a single value.

Browser support is now mature, the tools are excellent, and integration with the modern CSS ecosystem (variables, calc(), Tailwind) is native.

If you’re building something today, there’s no real reason not to start with OKLCH.