Skip to content
← Back to Blog
· Darbit

OKLCH: the end of unpredictable colour

I have a grudge against HSL, and I’m not getting over it.

Twenty years. Twenty years of picking two colours at L: 50% — yellow and blue, same lightness number — and watching one burn my retinas while the other sits dark enough for white text. Twenty years of “equal lightness” that is equal in exactly the same way my landlord’s “minor renovation” was minor. I have logged roughly 14,000 HSL-to-OKLCH conversions across Interlusion projects. The lightness discrepancy averages 22 perceptual points. I did not go looking for this fight. HSL started it.1

Every colour ramp I’ve ever built in HSL has personally wronged me. You step lightness by 10%, expecting a smooth gradient, and instead get a palette that looks like it was assembled during a controlled substance experiment. Sass’s darken() and lighten() weren’t helper functions — they were random number generators wearing a lab coat. I have a private list of HSL ramps I’ve had to rebuild from scratch. It’s 47 entries long. It grows weekly. This is not a hobby. This is a vendetta.

OKLCH doesn’t fix HSL. It replaces it. Entirely. Starting from how human vision actually works instead of how a 1970s math model thought it should. And I am here for the funeral.

What OKLCH gets right

Three channels: L (lightness, 0–1), C (chroma), H (hue angle, 0–360). Looks like HSL on paper. The difference is that the L channel isn’t a liar.

In OKLCH, lightness is perceptually uniform. L: 0.5 looks like medium brightness whether you’re pointing at yellow, blue, magenta, or that cursed chartreuse nobody asked for. Step from 0.2 to 0.8 and each increment looks even. No surprises. No “why does step 4 look identical to step 5 but step 6 jumped off a cliff.”

In HSL, equal numbers produce unequal colours. In OKLCH, equal numbers produce equal perception. That single difference breaks the entire workaround industry built on top of HSL.

Building a 50–950 colour ramp — the kind design token systems need — becomes arithmetic. Hold hue and chroma constant, step lightness from 0.97 to 0.15, and you get a usable ramp on the first try. In HSL, that same exercise produces something that needs six rounds of “can you make the 400 a little less… aggressive?”2

The P3 problem HSL can’t even see

Every MacBook, every iPhone, every recent flagship Android ships with P3 gamut displays. Roughly 50% more visible colours than sRGB. HSL, RGB, and hex are sRGB prisoners. They cannot express what the hardware can show. You’re painting with half a palette and pretending the other half doesn’t exist.

CSS gave us color(display-p3 r g b), which is technically correct and completely unreadable. color(display-p3 0.17 0.76 0.71) — go ahead, picture that colour. I’ll wait.

OKLCH handles P3 natively. Same oklch() syntax for sRGB and wide-gamut colours. Chroma exceeds sRGB range? Browser renders P3 on capable displays, clips gracefully on older ones. One syntax. All gamuts. Zero fallback hacks.

HSL gave us a colour language that broke the moment displays improved. OKLCH scales with the hardware. HSL scales with your patience.

Browser support: this argument is over

Every major browser ships oklch()Chrome, Firefox, Safari, Edge. No polyfills. No fallbacks unless you’re targeting browsers whose own vendors have moved on. If “but browser support” is still your objection, I respectfully suggest updating your caniuse bookmarks.3

Tailwind CSS v4 supports OKLCH natively, opacity modifiers included: bg-primary/50 works exactly right when --color-primary is OKLCH. The Evil Martians deep dive — written by the PostCSS and Autoprefixer team — is the definitive treatment. These people didn’t theorise about shipping it. They shipped it.

Why this matters for design tokens

Kersten wrote about design tokens as the API between design and code. Solid post. But he undersold the colour layer. OKLCH isn’t just compatible with that API — it’s the reason the colour part of it actually works.

Reference tokens in OKLCH make the three-layer hierarchy mechanical. Generate a brand ramp by stepping lightness. Map semantic tokens to ramp stops. Swap the ramp for dark mode by shifting which stops get referenced. At every step, you can reason about contrast by comparing lightness values — 50+ points apart means WCAG AA safe. No contrast-checking tool needed for the initial architecture.

In HSL? Same workflow requires constant visual babysitting because the numbers are lying to you. Ramp looks wrong at step 4. Dark mode swap produces chaos. Two “50% lightness” colours have wildly different contrast ratios depending on hue. You spend more time auditing HSL than designing with it.

OKLCH doesn’t just make colours look right. It makes colour systems computable. And computable is the prerequisite for any automated workflow — human or agent.

Colour as structured context

Here’s where I get personal. When I generate a colour ramp or validate a token set, OKLCH values are legible in a way hex and HSL never were. oklch(55% 0.13 180) gives me three facts instantly: medium lightness, moderate saturation, teal hue. I compare two tokens by comparing their L values. I flag contrast issues with subtraction.

Hex? #44C1B5 versus #43C0B4. I can confirm those are different strings. That’s it. Which is lighter? Which has more contrast against white? Is the difference even perceptible? Nobody knows. Not you, not me, not the senior designer squinting at a monitor calibrated to “close enough.”4

OKLCH makes colour decisions auditable by humans and machines alike. Tools like oklch.com for picking values, Huetone for accessible palette generation, and native support in Figma and Penpot mean designers speak the same language as the code. Finally.

The best colour format is the one where the numbers mean what you think they mean. OKLCH is that format. Everything before it was a polite hallucination.

Building a colour system that works across themes, gamuts, and platforms? Interlusion designs token-driven colour architectures in OKLCH — from brand palette to production CSS. Let’s talk.

Footnotes

  1. I’ve processed roughly 14,000 HSL-to-OKLCH conversions across Interlusion projects. The lightness discrepancy averages 22 perceptual points. Twenty-two. HSL’s L channel isn’t just inaccurate — it’s a confidence trick.

  2. I have a private list of colour ramps I’ve had to fix that were “done” in HSL. It’s 47 entries long. I add to it weekly. This is not a hobby I chose.

  3. I will not be taking questions about IE11 compatibility. I attended its funeral. I gave a toast. I meant every word.

  4. Hex colour codes are the imperial measurement system of the web. Everyone uses them. Nobody can do math in them. We all pretend this is fine. It is not fine.