Skip to content

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.

hsl(h 85% 65%)
oklch(0.65 0.112 h)

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.

in srgb
in oklch

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.

6.5:1 contrast

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.

app.tsx
<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

Search concepts

Search for a command to run...