CSS Color Spaces and Modern Color Functions
Use oklch, lch, lab, and color-mix() to create perceptually uniform color palettes, smooth gradients, and dynamic mixing.
Overview
Key Points
- •oklch(L C H) where L is lightness 0-1, C is chroma 0-0.4, H is hue 0-360
- •oklch is perceptually uniform: +0.1 lightness always looks the same visual step
- •color-mix(in colorspace, color1 pct%, color2) mixes two colors in a given color space
- •display-p3 has a wider gamut than sRGB, showing more vivid colors on P3 displays
- •Relative color syntax: oklch(from var(--base) l c calc(h + 30)) rotates the hue
- •color() function targets any color space: color(display-p3 r g b)
- •Use @supports (color: color(display-p3 0 0 0)) to feature-detect wide gamut support
- •Gradients in oklch avoid the gray muddy midpoint issue of HSL gradients
Examples
Building a palette with oklch
:root {
/* One hue, varying lightness (perceptually even steps) */
--indigo-100: oklch(0.93 0.05 264);
--indigo-200: oklch(0.85 0.09 264);
--indigo-300: oklch(0.75 0.13 264);
--indigo-400: oklch(0.65 0.17 264);
--indigo-500: oklch(0.55 0.20 264); /* base */
--indigo-600: oklch(0.45 0.20 264);
--indigo-700: oklch(0.35 0.18 264);
--indigo-800: oklch(0.27 0.14 264);
--indigo-900: oklch(0.20 0.09 264);
}Each step is exactly 0.1 lightness apart, so the palette looks visually even. In HSL you would need irregular increments to achieve the same result.
color-mix() for hover states and tints
:root {
--brand: oklch(0.55 0.2 264);
}
.btn {
background: var(--brand);
color: white;
}
.btn:hover {
/* Mix with black for a darker hover */
background: color-mix(in oklch, var(--brand) 80%, black);
}
.badge {
/* Tinted background: 20% brand + 80% white */
background: color-mix(in oklch, var(--brand) 20%, white);
color: var(--brand);
}
.overlay {
/* Semi-transparent variant */
background: color-mix(in srgb, var(--brand) 15%, transparent);
}Wide gamut display-p3 with sRGB fallback
.hero-text {
/* Fallback for older browsers (sRGB) */
color: #5b21b6;
/* Wide gamut purple: more vivid on P3 displays */
@supports (color: color(display-p3 0 0 0)) {
color: color(display-p3 0.28 0.08 0.80);
}
}
/* Or use media query for display gamut */
@media (color-gamut: p3) {
.hero-accent {
background: color(display-p3 0.20 0.80 0.50);
}
}Relative color syntax for dynamic theming
:root {
--primary: oklch(0.55 0.2 264);
}
.card {
/* Lighter version: +0.3 lightness, less chroma */
--card-bg: oklch(from var(--primary) calc(l + 0.38) calc(c * 0.3) h);
background: var(--card-bg);
}
.btn-primary {
background: var(--primary);
/* Darker on hover by reducing lightness */
--hover: oklch(from var(--primary) calc(l - 0.08) c h);
}
.btn-primary:hover {
background: var(--hover);
}Relative color syntax (oklch from) lets you derive variants from a single source color using math, making theming trivial.
Common Mistakes
- Using very high chroma values in oklch (above 0.37) which can exceed sRGB gamut and clip on standard monitors
- Forgetting that oklab is the Cartesian form of oklch so they share the same color model but different coordinate formats
- Expecting color-mix to work in all browsers without a fallback (use @supports)
- Generating gradients in HSL and getting a muddy midpoint; use oklch gradients instead
Note
Start your design system with a single oklch hue and vary only the lightness (0.2 to 0.95) and chroma (0.05 to 0.25) to generate a full palette. Every color will look visually balanced because oklch lightness is perceptually uniform.
Exercises
Write CSS to style the HTML scaffold. The preview updates live — when it looks right, mark as done.
Completed: 0 / 3 exercises
Knowledge Check
Question 1 of 3