OKLCH
In HSL, lightness is a number the math likes and your eyes don't. A yellow at 65% looks bright and a blue at 65% looks far darker. OKLCH defines lightness the way people see it, so one value looks about as light on every hue.
Pick colors in OKLCH, so equal lightness numbers actually look equally light.
Switch to lightness only and the HSL row turns into uneven grays, while the OKLCH row stays flat. That unevenness is why text passes contrast on one hue and fails on the next, and why some badges look louder than others.
The three numbers
oklch(L C H) is lightness, chroma and hue. Lightness runs from 0 (black)
to 1 (white). Chroma is how colorful it is, from 0 (gray) up to about
0.32 in sRGB and 0.36 on a P3 screen. Hue is an angle: around 25 is
red, 145 green and 250 blue.
Change one number and only that thing changes. Set chroma to 0 and any hue
becomes a gray at the same lightness.
Gradients
The color space also decides what happens between two colors. In sRGB, a gradient between opposite hues runs straight through gray. In OKLCH it keeps its chroma and turns through the hues in between.
You get this with one keyword: linear-gradient(in oklch, ...).
Palettes
Because lightness is reliable, a tonal scale is just a list of lightness values on one hue. Drag the hue and the ramp keeps its shape.
White text on the 700 step stays between 6.1:1 and 7.1:1 contrast on every
hue. White on hsl(h 70% 50%) swings from 1.5:1 to 8.9:1.
Hues can't all hold the same chroma, though. At lightness 0.93, sRGB fits
a green with almost four times the chroma of a blue. The demo caps every step
at what sRGB can show, which is why some hues come out softer.
Usage
Tailwind v4 already defines its palette in OKLCH, and the color tokens on this site are all OKLCH too.
<button class="bg-[oklch(0.48_0.13_250)] text-white hover:bg-[oklch(0.38_0.1_250)]">
Continue
</button>
<span class="bg-[oklch(0.93_0.05_145)] text-[oklch(0.48_0.12_145)]">
Shipped
</span>:root {
--brand-200: oklch(0.93 0.03 250);
--brand-700: oklch(0.48 0.13 250);
--brand-800: oklch(0.38 0.1 250);
}
.button {
background: var(--brand-700);
}
.button:hover {
background: var(--brand-800);
}
.hero {
background: linear-gradient(in oklch, oklch(0.6 0.16 260), oklch(0.75 0.16 70));
}oklch() has worked in every major browser since 2023, so it needs no
fallback. The catch is gamut. A value the screen can't show gets mapped back
in, and its lightness can shift on the way, so check new colors at
oklch.com before you ship them.
Resources
OKLCH in CSS, why we moved from RGB and HSL - The case for OKLCH, with side by side palettes that make the HSL problem obvious.
OKLCH color picker - Pick colors by lightness, chroma and hue, and see where the sRGB and P3 gamuts end.
oklch() - Syntax, ranges and browser support for the CSS function.
A perceptual color space for image processing - Björn Ottosson's post that introduced Oklab, the space OKLCH is built on.