Leverage perceptual color spaces, relative color syntax and typed math functions to build design systems the browser computes for you.
Open this lesson in KodokonThe 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.
: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));
}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.
.badge {
background: color-mix(in oklch, crimson 70%, white);
border-color: color-mix(
in oklch,
var(--brand),
transparent 60%
);
}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.
h1 {
font-size: clamp(1.5rem, 1.1rem + 2vw, 3rem);
}
.sidebar {
width: min(30%, 20rem);
padding-left: max(1rem, env(safe-area-inset-left));
}