CSS color handling has evolved dramatically. Learn oklch() for perceptually uniform colors, color-mix() for semantic theming, and how to build light/dark mode systems with CSS custom properties.
CSS has supported rgb() and hsl() for decades, but both have a critical flaw: they are not perceptually uniform. In HSL, increasing lightness from 50% to 60% looks very different depending on the hue. Blue appears much darker than yellow at the same HSL lightness value.
This makes generating programmatic color scales (like Tailwind's 50-900 range) inconsistent — the steps look uneven to the human eye.
oklch() is based on the Oklab perceptual model, designed so that equal numeric steps produce equal perceived differences in lightness and chroma.
/* oklch(lightness chroma hue) */
/* Lightness: 0-1 */
/* Chroma: 0-0.4+ (saturation) */
/* Hue: 0-360 */
.primary {
color: oklch(0.6 0.2 250); /* Medium-light, saturated blue */
}
.subtle {
color: oklch(0.8 0.05 250); /* Light, desaturated blue */
}
/* Generating a consistent color scale */
.swatch-100 { background: oklch(0.95 0.03 250); }
.swatch-200 { background: oklch(0.88 0.06 250); }
.swatch-300 { background: oklch(0.78 0.10 250); }
.swatch-400 { background: oklch(0.65 0.15 250); }
.swatch-500 { background: oklch(0.52 0.20 250); }
.swatch-600 { background: oklch(0.42 0.18 250); }
.swatch-700 { background: oklch(0.32 0.14 250); }
.swatch-800 { background: oklch(0.22 0.10 250); }
.swatch-900 { background: oklch(0.15 0.06 250); }
Each step in this scale looks visually equidistant — something impossible to achieve with HSL.
Browser support: Chrome 111+, Firefox 113+, Safari 15.4+. Use @supports for fallbacks.
color-mix() blends two colors by a percentage — powerful for generating hover states, disabled states, and transparency without JavaScript.
:root {
--brand: #2563eb;
}
.button {
background: var(--brand);
}
.button:hover {
/* 10% darker on hover */
background: color-mix(in oklch, var(--brand) 90%, black);
}
.button:disabled {
/* 40% transparent */
background: color-mix(in oklch, var(--brand) 60%, transparent);
}
/* Tinted background */
.alert-info {
background: color-mix(in srgb, #2563eb 10%, white);
border-color: color-mix(in srgb, #2563eb 40%, white);
}
This is exactly how Tailwind CSS v4's color-mix utilities work under the hood.
The best approach: define semantic tokens that change meaning in dark mode, rather than having components specify both light and dark values.
:root {
/* Base palette */
--blue-500: oklch(0.52 0.20 250);
--blue-400: oklch(0.65 0.15 250);
/* Semantic tokens — light mode */
--bg: oklch(0.98 0 0); /* near-white */
--surface: oklch(1 0 0); /* white */
--text-primary: oklch(0.15 0 0); /* near-black */
--text-muted: oklch(0.45 0 0); /* gray */
--brand: var(--blue-500);
--brand-hover: color-mix(in oklch, var(--brand) 90%, black);
}
.dark {
/* Semantic tokens — dark mode (same names, different values) */
--bg: oklch(0.12 0 0); /* near-black */
--surface: oklch(0.17 0 0); /* dark gray */
--text-primary: oklch(0.93 0 0); /* near-white */
--text-muted: oklch(0.65 0 0); /* light gray */
--brand: var(--blue-400); /* lighter blue for dark bg */
--brand-hover: color-mix(in oklch, var(--brand) 90%, white);
}
With this system, components never reference light/dark directly:
.card {
background: var(--surface);
color: var(--text-primary);
border-color: color-mix(in oklch, var(--text-muted) 20%, transparent);
}
Coming to all browsers in 2025: create color variations from a base color without color-mix():
.element {
--base: oklch(0.52 0.20 250);
/* 20% lighter version */
color: oklch(from var(--base) calc(l + 0.2) c h);
/* Desaturated */
border-color: oklch(from var(--base) l calc(c * 0.3) h);
/* Complementary (opposite hue) */
accent-color: oklch(from var(--base) l c calc(h + 180));
}
Explore color relationships interactively with HeoLab's Color Palette Generator — generate harmonious palettes from any base color.
oklch() gives you perceptually consistent color scales that HSL cannot. color-mix() eliminates the need for separate hover/disabled/transparent color variables. Combined with CSS custom properties for semantic tokens, you can build a complete design system that correctly adapts to light/dark mode without any JavaScript color manipulation.