Skip to content
C Codeloom
HTML & CSS

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.

·4 min read · By Codeloom
Intermediate 8 min read

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() replaces hsl() 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-p3 and rec2020 extend 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.
Palette derivation from a single base color

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.