Color in CSS: Modern Color in the Browser
For two decades CSS color meant hex codes and rgb() - a flat box of sRGB crayons. That
era is over. Modern CSS can blend colors with color-mix(), derive variants with
relative color syntax, reach beyond sRGB with color() and Display-P3, and finally
describe color in a perceptually even way with oklch(). This is the hands-on guide to
the color toolkit your browser already ships.
Color in CSS grew up
The original CSS color model was simple and small: named colors, #rrggbb hex, and
rgb() - all of it locked inside the sRGB gamut and all of it
numeric. If you wanted a color 10% lighter, you did the arithmetic yourself or reached for
a preprocessor like Sass. If you wanted a color your screen could show but sRGB could not name, you
were out of luck.
CSS Color Level 4 and 5 changed all of that, and the features have quietly shipped
across every major browser. You can now compute with color directly in the stylesheet:
mix two colors in a color space of your choosing, build a new color from the channels of an
existing one, address wide-gamut displays, and specify color in spaces - lab(),
lch(), oklab(), oklch() - that match how people actually
see. CSS stopped being a list of crayons and became a small color engine.
Beyond hex: the new notations
Before the verbs (mixing, deriving) come the nouns - the color notations themselves. Modern CSS accepts a whole family, and knowing which space each one lives in is the key to using them well. They fall into three groups: legacy sRGB, ergonomic cylinders, and perceptual spaces.
color(display-p3 …),
color(rec2020 …), color(srgb-linear …), and more.r g b,
l c h, l a b) become usable references - the basis of relative
color syntax in section 4.The mental model: sRGB notations describe a device; Lab/OKLab notations describe a perception. When you compute with color - interpolating, lightening, building scales - you almost always want to do it in a perceptual space and only convert back to a device color at the very end.
Blending with color-mix()
color-mix() takes two colors, a percentage, and - crucially - a
color space to mix in: color-mix(in oklch, #2f6df6 60%, #f6c12f)
means "60% of the blue, 40% of the gold, interpolated through OKLCH." The space is not a detail;
it changes the path the blend takes. Mix blue and yellow in srgb and you pass through
a dead gray midpoint; mix the same pair in hsl and you swing through green; mix in
oklch and you get an even, controlled transition. Try all three.
The same two colors, three interpolation spaces
Pick two endpoint colors and the space to mix in. The strip shows the full blend from the
first color to the second; the marker sits at your chosen percentage. Switch the space and
watch the path change - srgb often dips through mud, hsl
detours through other hues, oklch stays clean. The generated CSS updates live.
color-mix(in oklch, var(--brand) 85%, black) is a
rock-solid "slightly darker brand" - and … 85%, white) a matching tint. One source
color, a whole consistent set of states, no Sass.
Relative color syntax
color-mix() blends toward another color; relative color syntax lets
you reach inside a color and rewrite its channels. The shape is
oklch(from <color> L C H): the from keyword exposes the source's
channels as the keywords l, c, and h, which you can pass
through untouched or wrap in calc(). Want the brand color but 10% lighter? -
oklch(from var(--brand) calc(l + 0.1) c h). Keep lightness and hue, halve the chroma
for a muted version? - oklch(from var(--brand) l calc(c / 2) h). Adjust the channels
and read the generated rule.
Derive a new color from a source's channels
The left swatch is your source color; the right is the derived result. The three controls map
to the OKLCH channels: shift lightness, scale chroma, rotate hue. The generated
oklch(from …) rule is exactly what you would paste into a stylesheet - and if a
requested color falls outside sRGB, the readout tells you the chroma the browser would clip
it to.
Wide gamut and the color() function
Most phones and laptops sold today have Display-P3 screens - a gamut roughly 25%
larger than sRGB, with noticeably more saturated reds and greens. sRGB hex literally
cannot name those colors. The color() function can:
color(display-p3 1 0 0) is a P3 red far more vivid than #ff0000. You can
even guard it with @media (color-gamut: p3) to serve the punchy version only where it
will show.
The flip side is gamut mapping. When you ask for a color the target display can't
produce - say a very high-chroma oklch() on an sRGB screen - the browser has to bring
it back into range. The naive approach clips each RGB channel independently, which shifts both hue
and lightness. The CSS spec instead reduces chroma while holding lightness and hue, which
preserves the color's identity. See the difference.
When a color is too vivid for the screen
Dial up an OKLCH color. When its chroma exceeds what sRGB can show, the left half is the naive per-channel clip (notice it can drift in hue and lightness) and the right half is chroma-reduction mapping (same lightness, same hue, only less saturated). The bar shows requested chroma versus the sRGB ceiling at this lightness and hue.
Why oklch() is the new default
Here is the single most useful thing to internalize about modern CSS color: hsl()
lightness is a lie, and oklch() lightness is honest. In HSL, every hue claims the same
"50% lightness," yet hsl(60 100% 50%) (yellow) is blinding while
hsl(240 100% 50%) (blue) is nearly black. Build a palette by sweeping HSL hue at fixed
lightness and it lurches between bright and dark. Do the same in OKLCH and every swatch holds the
same perceived lightness - so contrast stays predictable and the set looks like a family.
Sweep the hue wheel at "constant" lightness
Both rows sweep hue from 0° to 360°. The top row is hsl(h 100% L); the bottom is
oklch(L 0.13 h). The white line over each row traces measured luminance. The HSL
line heaves up and down - bright yellows, dark blues - while the OKLCH line stays nearly flat.
The ΔY readout is the spread between the lightest and darkest point of each row: smaller
is more even.
oklch(); keep hsl() for quick one-off tweaks where perceptual evenness
doesn't matter; reserve hex for fixed, externally-specified brand colors.
Best practices and pitfalls
The new functions are powerful, but they reward a little discipline. A handful of habits keep a modern-color stylesheet predictable across browsers, gamuts, and themes.
color-mix() to in oklch for tints,
shades, and gradients - sRGB interpolation muddies mid-tones.color-mix() from one token, so a brand change ripples everywhere.color(display-p3 …) behind
@media (color-gamut: p3) with an sRGB fallback first.oklch() may render less saturated on sRGB. Test
on a real sRGB screen; don't assume the slider value is what ships.rgb() declaration before a modern one so old
engines get something usable, then let the cascade upgrade it.oklch() tells the eye what to feel - and lets
the browser worry about the voltage. That inversion is the whole story of modern CSS color."
Editorial summary · from device to perception
oklch(), oklab(),
color(), and color-mix() are supported across current Chrome, Edge,
Safari, and Firefox; relative color syntax landed last and is now broadly available in up-to-date
browsers. As always, lead with a fallback and treat the modern value as a progressive enhancement.
Test your understanding
Six questions on color-mix(), relative color syntax, wide gamut, gamut mapping, and oklch(). Instant feedback, no scores recorded - a wrong answer comes with a short explanation.
Quick check
Continue your journey
Modern CSS color is the practical front end of a deeper stack - the perceptual models behind it, the gamuts it targets, and the systems it builds.
Oklab and Oklch: Modern Perceptual Color Spaces
The perceptual model that powers oklch() and oklab() in CSS.
Digital · 21HSL, HSV, and HSB: The Cylindrical Color Models
Where CSS hsl() comes from - and why its lightness misleads.
Digital · 12RGB, sRGB, Adobe RGB, ProPhoto, Display P3, and Rec.2020
The wider gamuts the color() function can finally reach.
Colorimetry · 37Gamut Mapping
What the browser does when a color is too vivid to show.
Design · 36Building a Color System: Scales, Tokens, Themes
Turning these color functions into tokens and themes.
Design · 13Accessible Color Design and WCAG Contrast
Keeping every derived CSS color above the contrast bar.