CSS Modern Color Functions
A practical tour of modern CSS color: oklch, color-mix, relative color syntax, and wide-gamut color spaces. Learn how to pick palettes that stay perceptually even and accessible across themes.
What you'll learn
- ✓Why oklch beats hsl for designing palettes
- ✓How color-mix() composes new colors at runtime
- ✓How relative color syntax derives shades from a base
- ✓Wide-gamut color and what display-p3 buys you
- ✓Pitfalls around fallbacks and accessibility
Prerequisites
- •Basic familiarity with CSS color values
What and Why
CSS color used to mean hex codes and the occasional rgba(). Today the platform ships a richer set: oklch(), color-mix(), relative color syntax with from, and wide-gamut spaces like display-p3. These features matter because the old model was tied to the sRGB monitor of the late nineties. Modern displays show more colors, and modern apps need themable, accessible palettes that scale.
The goal of this post is practical: pick the right function for the job, avoid the traps, and stop hand-tuning hex codes.
Mental Model
Group the new tools by what they replace:
oklch()replaceshsl()as the palette-design function. It is perceptually uniform, so equal lightness numbers actually look equally light.color-mix()replaces ad-hoc opacity tricks and Sass mix functions. It blends two colors in a chosen color space at runtime.- Relative color syntax (
oklch(from var(--brand) calc(l - 0.1) c h)) replaces a wall of variant variables. You derive shades from a single base. display-p3andrec2020extend the color gamut beyond sRGB on capable displays.
You can mix and match. The browser falls back gracefully on older targets if you provide a plain color first.
Hands-on Example
Define a brand color in oklch and derive a hover and disabled variant from it. Then mix the brand into white to produce a soft background.
:root {
--brand: oklch(62% 0.18 250);
--brand-hover: oklch(from var(--brand) calc(l - 0.06) c h);
--brand-disabled: oklch(from var(--brand) l calc(c * 0.3) h);
--brand-bg: color-mix(in oklch, var(--brand) 12%, white);
}
.button {
background: var(--brand);
color: white;
}
.button:hover { background: var(--brand-hover); }
.button:disabled { background: var(--brand-disabled); }
.panel { background: var(--brand-bg); }
--brand (oklch 62% 0.18 250)
|
|-- --brand-hover = lightness - 0.06
|-- --brand-disabled = chroma * 0.3 (desaturated)
\-- --brand-bg = mix 12% brand into white
Change --brand -> all variants update automatically.
Want a wide-gamut accent for capable screens? Layer it:
.accent { color: #ff3366; }
@supports (color: color(display-p3 1 0 0)) {
.accent { color: color(display-p3 1 0.2 0.4); }
}
Common Pitfalls
oklch lightness is not the same as hsl lightness. A 50% L in hsl looks darker for some hues; in oklch it stays consistent. Do not copy your old numbers across.
color-mix() defaults can surprise you. Mixing in srgb versus oklch produces different middle colors, especially across hue jumps. Specify the space explicitly: color-mix(in oklch, red, blue).
Relative color syntax requires the base to actually resolve. Chained var() references that loop or fail silently will collapse the whole computation. Test in the inspector when something looks wrong.
Wide-gamut colors are clipped on sRGB-only screens, but the clipping is not always what you expect. Use @supports and design the sRGB version first, then enhance.
Finally, none of this guarantees contrast. A pretty oklch palette can still fail WCAG. Run your text and background combinations through a contrast checker.
Best Practices
Pick one design space for the system and stick to it. oklch is the safest default in 2026.
Define a small set of tokens (brand, surface, text, plus state variants) and derive the rest with relative color syntax or color-mix(). Designers can tweak one number and the whole UI updates.
Always specify the interpolation space in color-mix(). The default may change between engines and the explicit form documents intent.
Provide an sRGB-safe baseline and gate enhancements behind @supports. Users on older devices still get a working palette.
Audit contrast at every step, especially for hover and disabled variants. Perceptual uniformity helps, but it is not a contrast guarantee.
Wrap-up
Modern CSS color turns palette work from manual hex-tuning into a small system of tokens and transforms. Use oklch for design, color-mix() for blends, relative color syntax for variants, and wide-gamut spaces for richer screens. Keep fallbacks honest and contrast checked, and your themes will hold up across devices and modes.
Related articles
- HTML & CSS CSS Cascade Layers and Specificity: A Calmer Mental Model
Stop fighting !important. Learn how the CSS cascade, specificity, and the new @layer rule combine to give you predictable, maintainable styles.
- HTML & CSS CSS Clamp and Fluid Typography
Use the CSS clamp function to build fluid typography and spacing that scales smoothly between breakpoints without media queries or jarring jumps.
- HTML & CSS CSS Container Queries Explained with Real Examples
Container queries let components style themselves based on their parent's size, not the viewport. Learn the syntax, the units, and when to use them.
- HTML & CSS CSS Grid vs Flexbox: When to Use Each (with Examples)
Stop flipping a coin between Grid and Flexbox. Learn the mental model that picks the right layout tool every time, with practical CSS examples.