Modern colour: oklch, color-mix, relative colour

CSS · Expert · 12 min

Generating hover states, tints and themes from one brand colour removes hand-maintained colour tables.

CSS colour has moved well beyond sRGB hex.

Colour spaces. rgb() and hsl() describe sRGB, which is non-perceptual: hsl(60 100% 50%) yellow looks far lighter than hsl(240 100% 50%) blue at the same "lightness". oklch(L C H) and oklab() are perceptually uniform: equal L means equal perceived lightness, which makes accessible palettes and consistent contrast much easier. color(display-p3 1 0 0) and high-chroma oklch values reach wide-gamut colours on modern screens. Out-of-gamut colours are mapped to what the display can show, so give an sRGB fallback where precision matters.

color-mix() blends two colours in a chosen space: color-mix(in oklch, var(--brand), white 20%). Mixing in oklab or oklch avoids the muddy grey midpoints of sRGB mixing.

Relative colour syntax derives a colour from another: oklch(from var(--brand) calc(l - 0.1) c h) is a darker brand for hover, and rgb(from var(--brand) r g b / 50%) adds transparency. One token generates a whole scale.

light-dark() picks a value by the element's color-scheme: color: light-dark(#111, #eee), with color-scheme: light dark on :root.

Interpolation space matters in gradients too: linear-gradient(in oklch, blue, yellow) avoids the grey middle that sRGB gradients produce.

Finally, check contrast: perceptual lightness helps, but WCAG contrast ratios must still be checked with tools.

Example

:root {
  color-scheme: light dark;
  --brand: oklch(55% 0.2 265);                       /* indigo */
  --brand-hover: oklch(from var(--brand) calc(l - 0.08) c h);
  --brand-soft: color-mix(in oklch, var(--brand), white 85%);
  --brand-50a: oklch(from var(--brand) l c h / 50%);
  --bg: light-dark(#ffffff, #0b1020);
  --fg: light-dark(#111827, #e5e7eb);
}
body { background: var(--bg); color: var(--fg); }
.button { background: var(--brand); }
.button:hover { background: var(--brand-hover); }
.tag { background: var(--brand-soft); }

.hero { background: linear-gradient(in oklch, oklch(70% 0.2 30), oklch(70% 0.2 250)); }

@media (color-gamut: p3) {
  .badge { background: color(display-p3 1 0.2 0.4); }
}

Remember

  • oklch is perceptually uniform: equal L looks equally light
  • color-mix(in oklch, ...) blends without muddy midpoints
  • Relative colour syntax derives hover and alpha variants from one token
  • light-dark() + color-scheme for simple theming

Watch out for

  • Assuming equal hsl lightness means equal perceived lightness or contrast.

Check yourself

  1. What does oklch(from var(--c) l c calc(h + 180)) produce?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/oklch

Builds on

Unlocks

Nothing builds on this yet.