Kodokon kodokon.com

Colors and advanced functions: oklch, color-mix, min/max

Leverage perceptual color spaces, relative color syntax and typed math functions to build design systems the browser computes for you.

10 min · 3 questions

Open this lesson in Kodokon

The structural flaw of hsl(): its lightness is not perceptual. hsl(60 100% 50%) (yellow) looks dazzling while hsl(240 100% 50%) (blue) looks dark, at identical L. oklch fixes this: its L component (0 to 1) measures perceived lightness, C is the chroma (absolute saturation, 0 to roughly 0.37) and H the hue angle. Changing H at constant L preserves perceived brightness: your palette variations finally become predictable. Combined with the relative color syntax (from), the browser derives your hover, muted or dark variants by itself.

CSS
:root {
  --brand: oklch(0.65 0.19 255);
  --dark: oklch(from var(--brand) calc(l - 0.15) c h);
  --muted: oklch(from var(--brand) l calc(c / 2) h);
  --accent: oklch(from var(--brand) l c calc(h + 180));
}
A single source color, three computed derivatives: the l, c, h channels are exposed as local variables.

oklch can describe colors outside the sRGB gamut, or even outside Display-P3. What does the browser do when the screen cannot display them? Not a brutal channel-by-channel clip: the specification prescribes a gamut mapping that progressively reduces chroma while preserving lightness and hue, until the color fits the target gamut. You can target wide-gamut screens explicitly with @media (color-gamut: p3). Another mixing tool: color-mix(), whose first argument - the interpolation space - genuinely changes the result: an in srgb mix of blue and yellow turns gray, while in oklch travels through vivid hues.

CSS
.badge {
  background: color-mix(in oklch, crimson 70%, white);
  border-color: color-mix(
    in oklch,
    var(--brand),
    transparent 60%
  );
}
Mixing with transparent produces a translucent variant without duplicating the color as rgba.

min(), max() and clamp() are typed math functions: they accept mixing units that are incompatible at parse time (rem + vw) because resolution happens at used-value time, once the viewport is known. clamp(MIN, VAL, MAX) is strictly equivalent to max(MIN, min(VAL, MAX)) - a spec consequence: if MIN exceeds MAX, MIN wins. For fluid typography, always include a rem component in the middle value: a font-size: clamp(1rem, 2.5vw, 3rem) without rem would ignore the user's text zoom, a genuine accessibility problem.

CSS
h1 {
  font-size: clamp(1.5rem, 1.1rem + 2vw, 3rem);
}
.sidebar {
  width: min(30%, 20rem);
  padding-left: max(1rem, env(safe-area-inset-left));
}
Accessible fluid typography and gutters that respect the notches of mobile screens.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What decisive advantage does oklch offer over hsl?
    • A shorter syntax to write
    • An L lightness that is perceptually uniform from one hue to another
    • A guarantee of staying within the sRGB gamut
    • The only color space to support an alpha channel
  2. What does the browser do when an oklch color exceeds the screen's gamut?
    • It ignores the declaration and keeps the previous color
    • It applies a gamut mapping that mainly reduces the chroma
    • It clips each RGB channel independently, with no further processing
  3. In clamp(MIN, VAL, MAX), what happens if the computed MIN value exceeds MAX?
    • The declaration becomes invalid and is ignored
    • MAX wins and caps the result
    • MIN wins, because clamp expands to max(MIN, min(VAL, MAX))