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:
- L — real perceptual Lightness (0–1)
- C — Chroma, i.e. “how saturated” (roughly 0–0.4)
- H — Hue, in degrees (0–360°)
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:
0°–30°→ reds/pinks60°–120°→ yellows/greens180°–250°→ cyans/blues300°–360°→ purples/magentas
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:
- Chrome/Edge: full support since v111 (March 2023)
- Firefox: full support since v113 (May 2023)
- Safari: full support since v15.4 (March 2022) — Safari was ahead of everyone else
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
- oklch.com — The reference picker. Lets you explore the OKLCH space interactively and copy CSS values directly.
- oklch.evilmartians.io — Evil Martians’ tool with a built-in palette generator.
- Colour & Contrast — WCAG contrast checking with OKLCH support.
Libraries and integrations
- Color.js — A full-featured JavaScript library for conversions and manipulation across color spaces, including OKLCH. Developed by Lea Verou and Chris Lilley.
- Tailwind CSS v4 — Tailwind has natively adopted OKLCH for its default palette. If you’re using Tailwind v4, you’re already using OKLCH.
- postcss-oklab-function — PostCSS plugin for automatic fallback.
Further reading
- CSS Color Level 4 — The W3C specification that defines OKLCH and the other modern color spaces.
- “OKLCH in CSS: why we moved from RGB and HSL” — Evil Martians Blog, the reference technical article on adopting OKLCH in production.
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.