Skip to content
Aback UI

ColorCSS

OKLCH colours in CSS for design systems

What OKLCH is, why Tailwind CSS v4 uses it, how it compares with HEX and HSL, and how to build an even colour scale from one brand colour.

By the Aback UI teamPublished 8 min read

OKLCH is a CSS colour format that describes a colour by perceptual lightness (L), chroma (C), and hue (H). Unlike HSL, equal lightness values look about equally light across hues, which makes it far easier to build even colour scales and predictable light and dark themes. Tailwind CSS v4 defines its default palette in OKLCH, and every major browser supports oklch().

What is OKLCH?

OKLCH is the polar form of the Oklab colour space, published by Björn Ottosson in 2020 and included in CSS Color Level 4. Oklab was designed so that numeric distances track perceived differences more closely than older models. OKLCH expresses the same space with three intuitive axes:

  • L, lightness: 0 is black, 1 (or 100%) is white.
  • C, chroma: how colourful it is. 0 is grey. Practical values for screen colours stay below about 0.4.
  • H, hue: an angle from 0 to 360 degrees around the colour wheel.
css
:root {
  /* Aback Blue, #009bf9, written in OKLCH */
  --brand: oklch(66.99% 0.1748 246.65);

  /* With alpha */
  --brand-wash: oklch(66.99% 0.1748 246.65 / 0.1);
}

The MDN reference for oklch() (opens in a new tab) covers the syntax in full. To convert an existing colour, paste it into the colour converter, which outputs HEX, RGB, HSL, and OKLCH.

OKLCH vs HEX and HSL

HEX and RGB describe how much red, green, and blue light a display emits. They are exact, compact, and universally supported, but you cannot read lightness or hue from them, and adjusting one channel changes everything at once.

HSL looks like a fix: it has a lightness axis. The problem is that HSL lightness is a simple function of the RGB channels, not of how the eye perceives brightness. Pure colours at the same HSL lightness look very different:

ColourHSLOKLCH lightness
#ffff00 yellowhsl(60 100% 50%)96.8%
#00ff00 greenhsl(120 100% 50%)86.6%
#0000ff bluehsl(240 100% 50%)45.2%

All three are 50% in HSL. In OKLCH, yellow is almost white and blue is darker than mid-grey, which matches what you see. If you build two colour scales in HSL with the same lightness steps, a yellow 500 and a blue 500 will not look like the same weight, and their contrast against white will differ widely.

FormatPerceptual lightnessEasy to adjust by handBeyond sRGB
HEX / RGBNoNoNo
HSLNoYesNo
OKLCHApproximatelyYesYes

Why does Tailwind CSS v4 use OKLCH?

Tailwind CSS v4 redefined its default colour palette in OKLCH. The Tailwind colours documentation (opens in a new tab) lists every value as an oklch() colour. Two practical reasons stand out:

  • More vivid colours on capable displays. OKLCH can express colours in wider gamuts such as Display P3, so the palette can use more saturated colours on screens that support them.
  • Consistent steps. A predictable lightness axis makes it easier to keep a 500 in one hue visually comparable to a 500 in another.

Your own theme does not have to use OKLCH just because Tailwind does. Theme variables accept any CSS colour, and Aback UI's theme defines its roles in HEX. What matters is that the values you pick are deliberate and checked.

Wide gamut and out-of-range colours

Not every OKLCH value can be displayed. sRGB, the gamut of HEX colours and most monitors, is smaller than Display P3, which is smaller than what OKLCH can describe. When a colour falls outside the display's gamut, the browser maps it to the nearest colour it can show.

Three practical rules follow:

  1. Chroma limits depend on lightness and hue. A blue can hold far more chroma at mid lightness than near white or near black. A scale that keeps the same chroma at every step will push its lightest and darkest steps out of gamut.
  2. Do not rely on out-of-gamut colours for meaning. On an sRGB display, two vivid P3 colours can map to nearly the same visible colour.
  3. Check contrast on the colour people actually see. A contrast checker should convert to sRGB first. The colour converter gamut-maps OKLCH colours that fall outside sRGB.

How to build an even scale from one brand colour

A Tailwind-style scale has eleven steps, from 50 to 950. In OKLCH the recipe is straightforward:

  1. Convert the brand colour to OKLCH and note its hue and chroma.
  2. Give each step a target lightness, from near white at 50 to near black at 950.
  3. Keep the hue fixed.
  4. Scale chroma down toward the ends, where less colourfulness is displayable and pale tints look cleaner.
  5. Place the original brand colour at the step whose lightness is closest, unchanged, so the brand value appears exactly in the scale.
  6. Convert each step back to sRGB, reducing chroma further if it is out of gamut.

The Tailwind colour scale generator on this site follows this recipe. Its lightness targets are close to the Tailwind CSS v4 default palette:

Step50100200300400500600700800900950
L0.9750.940.8850.810.720.640.5550.470.3950.330.255
Chroma share12%26%48%72%90%100%96%86%74%62%46%

It exports the result as a Tailwind CSS v4 @theme block, a v3 config, or plain CSS variables. For Aback Blue, #009bf9, the @theme output is:

css
@theme {
  --color-brand-50: oklch(97.5% 0.0125 246.61);
  --color-brand-100: oklch(94% 0.0304 246.63);
  --color-brand-200: oklch(88.5% 0.0596 246.64);
  --color-brand-300: oklch(81% 0.1013 246.64);
  --color-brand-400: oklch(72% 0.1545 246.65);
  --color-brand-500: oklch(66.99% 0.1748 246.65);
  --color-brand-600: oklch(55.5% 0.1449 246.67);
  --color-brand-700: oklch(47% 0.1227 246.68);
  --color-brand-800: oklch(39.51% 0.1032 246.7);
  --color-brand-900: oklch(33.01% 0.0863 246.74);
  --color-brand-950: oklch(25.51% 0.0668 246.84);
}

The hue stays within a fraction of a degree, lightness follows the targets, and the 500 step is the input colour unchanged. The 50 step has less chroma than its 12% share would give, because the generator reduces chroma where sRGB cannot show it.

A ramp defined in HEX can drift

A scale picked by eye or by HSL steps often drifts in hue. Converting Aback UI's own Aback Blue ramp to OKLCH shows the hue moving from about 232 degrees at 50 to about 247 at 500. That is not wrong, since the ramp is a design decision, but OKLCH makes the drift visible and controllable. The ramp is listed on the colors page in every format.

Using OKLCH in a design system today

You can adopt OKLCH gradually. A few patterns are useful even if your stored tokens stay in HEX:

Mix in OKLCH

color-mix() takes a colour space argument. Mixing in OKLCH avoids the muddy, greyish midpoints that mixing in sRGB can produce:

css
.selected-row {
  background-color: color-mix(in oklch, var(--link) 12%, var(--background));
}

Derive variants with relative colour syntax

Relative colour syntax reads the channels of an existing colour so you can adjust one of them. Check browser support on MDN before relying on it:

css
.button-brand:hover {
  /* Same hue and chroma, slightly darker */
  background-color: oklch(from var(--brand-fill) calc(l - 0.06) c h);
}

Keep roles semantic

OKLCH changes how you choose values, not how components use them. Components should still read roles such as --link and --brand-fill, and the role decides which step of the scale applies in each theme. Aback UI's link role, for example, uses the 700 step in the light theme and the 400 step in the dark theme, because those are the steps that pass text contrast on each background. The pattern is explained in Design tokens in Tailwind CSS v4 with @theme, and the thresholds in WCAG colour contrast for UI developers.

To go from one brand colour to a complete set of light and dark roles, use the theme generator, then apply the output as described in the theming guide.

Frequently asked questions

Is OKLCH supported in all browsers?

The oklch() function is supported in current versions of all major browsers. Check MDN's compatibility data for the exact versions if you support older browsers, and provide a HEX fallback where needed.

Is OKLCH lightness the same as WCAG contrast?

No. OKLCH lightness is perceptual and correlates with contrast, but the WCAG contrast ratio uses a different luminance formula. Two colours with the same OKLCH lightness can have slightly different contrast ratios, so always check the actual pairs.

Should I convert my whole palette from HEX to OKLCH?

Not necessarily. HEX values still work and are the safest choice for sRGB colours. OKLCH helps most when you generate or adjust a scale, or when you want colours outside sRGB on wide-gamut displays.

What is the maximum chroma in OKLCH?

There is no fixed maximum. How much chroma a colour can have depends on its lightness, its hue, and the target gamut. Very light and very dark colours can hold much less chroma than mid tones.

Share this post

Try Aback UI in your project

Accessible React components you install as source. MIT licensed.

Get started