CSS color is undergoing its biggest transformation in a decade. The old trio of hex, rgb(), and hsl() served us well, but displays have outgrown sRGB, and our tools need to catch up. Enter CSS Color Level 4: OKLCH, color-mix(), relative color syntax, and native wide-gamut support.
By mid-2026, over 87% of browsers support OKLCH β including all major desktop and mobile browsers. This isn't a future technology; it's available in production today. In this guide, you'll learn how to use these modern CSS color features to build more perceptually consistent, maintainable, and vibrant design systems.
HSL seemed like a breakthrough when it arrived. Having hue, saturation, and lightness as intuitive controls was far better than memorizing hex codes. But HSL has a dirty secret: it's not perceptually uniform. Change the saturation in HSL, and the perceived brightness shifts visually even though the "lightness" value stays the same.
This is because HSL models the color space of sRGB, not the human visual system. Bright yellow appears much lighter than bright blue at the same HSL lightness value. When you try to generate a color palette by stepping HSL lightness uniformly, you get a lopsided result β some steps look too dark, others too light.
OKLCH solves this by modeling how humans actually perceive color. It was developed by BjΓΆrn Ottosson in 2020 and has since been adopted into the CSS specification. In OKLCH:
Before diving into syntax, it's essential to understand what each OKLCH channel does:
Lightness in OKLCH is perceptual β a value of 0.5 looks like 50% brightness regardless of the hue. This is dramatically different from HSL, where hsl(60, 100%, 50%) (yellow) looks much brighter than hsl(240, 100%, 50%) (blue). In OKLCH, oklch(0.5 0.15 60) and oklch(0.5 0.15 240) will look equally bright.
Chroma is the "purity" or intensity of a color, from 0 (grayscale) upward. Unlike HSL saturation, chroma has no fixed maximum β it depends on the hue and the color gamut. For sRGB, maximum chroma varies from about 0.13 (blue hues) to 0.32 (reds and greens). Display P3 can reach chroma values over 0.4 for many hues.
Hue works similarly to HSL: an angle from 0 to 360 on a color wheel. 0 is red, 90 is yellow-green, 180 is cyan-blue, 270 is purple, and 360 wraps back to red. Same familiar mental model, but now your other channels behave correctly.
/* OKLCH Syntax in CSS */ .example-1 { color: oklch(0.5 0.2 180); } /* L=0.5 (medium lightness), C=0.2 (moderate chroma), H=180 (cyan) */ .example-2 { color: oklch(70% 0.3 30deg); } /* Percentage lightness, deg hue, same effect */ .gray { color: oklch(0.5 0 0); } /* Chroma 0 = neutral gray at 50% lightness */
Notice that to get a pure gray, you simply set chroma to 0. No fiddling with equal R/G/B values or battling HSL saturation quirks.
Let's see the practical difference. Suppose you want to create seven evenly-spaced lightness steps of a blue color for a UI scale:
| Step | HSL Attempt | Perceptual Result | OKLCH Equivalent | Result |
|---|---|---|---|---|
| 100 | hsl(240, 100%, 95%) |
Very light (good) | oklch(0.95 0.03 270) |
Very light β |
| 200 | hsl(240, 80%, 80%) |
Light (good) | oklch(0.8 0.08 270) |
Light β |
| 300 | hsl(240, 70%, 65%) |
OK | oklch(0.65 0.12 270) |
Medium-light β |
| 400 | hsl(240, 60%, 50%) |
Looks very dark | oklch(0.5 0.15 270) |
Genuine medium β |
| 500 | hsl(240, 55%, 35%) |
Almost black | oklch(0.35 0.12 270) |
Dark β |
| 600 | hsl(240, 50%, 20%) |
Black with blue tint | oklch(0.2 0.06 270) |
Very dark β |
In HSL, the "50% lightness" step (hsl(240, 60%, 50%)) looks closer to a 30% darkness because blue appears darker than other hues at the same lightness value. In OKLCH, every step looks like its actual lightness. The difference is immediately visible when building UI color ramps.
color-mix() lets you blend two colors in any color space, with full control over mix proportions. This is a huge upgrade from working with Sass or Less mixins β it's native, dynamic, and can respond to CSS custom properties.
/* Basic 50/50 mix in the default (OKLCH) space */ .mixed { background: color-mix(in oklch, #ff6b6b, #4ecdc4); } /* Specify proportions */ .warm-mix { background: color-mix(in oklch, #ff6b6b 75%, #4ecdc4 25%); } /* Mix in different color spaces for different effects */ .srgb-mix { background: color-mix(in srgb, #ff6b6b, #4ecdc4); } .hsl-mix { background: color-mix(in hsl, #ff6b6b, #4ecdc4); } .lch-mix { background: color-mix(in lch, #ff6b6b, #4ecdc4); } /* Mix with CSS custom properties β dynamic theming! */ :root { --primary: #6c5ce7; --accent: #fd79a8; } .themed { color: color-mix(in oklch, var(--primary), var(--accent)); }
/* Light/dark variants using color-mix */ .btn-primary { background: color-mix(in oklch, var(--primary), white 10%); } .btn-primary:hover { background: color-mix(in oklch, var(--primary), black 15%); } /* State colors from a single brand color */ .bg-error { background: color-mix(in oklch, var(--primary), red 30%); } .bg-success { background: color-mix(in oklch, var(--primary), green 30%); } .bg-warning { background: color-mix(in oklch, var(--primary), yellow 40%); }
If you only learn one new CSS color feature this year, make it relative color syntax. This lets you take an existing color and modify specific channels β a task that previously required preprocessors, runtime JavaScript, or manual hex arithmetic.
Relative colors use the from keyword followed by a source color, then channel calculations:
/* Darken a color by reducing lightness */ .darkened { color: oklch(from var(--primary) calc(l - 0.15) c h); } /* Change the hue while keeping lightness and chroma */ .hue-shifted { color: oklch(from #6c5ce7 l c calc(h + 180)); } /* Desaturate a color (make it more gray) */ .muted { color: oklch(from #ff6b6b l calc(c * 0.3) h); } /* Generate a lighter version with custom alpha */ .lighter { color: oklch(from var(--primary) calc(l + 0.2) c h / 0.8); } /* Relative colors work in any supported space */ .hsl-tweak { color: hsl(from #6c5ce7 h s calc(l + 10%)); } /* Create a full color scale from one base color */ :root { --base: oklch(0.5 0.2 270); } --scale-100: oklch(from var(--base) 0.95 calc(c * 0.15) h); --scale-200: oklch(from var(--base) 0.85 calc(c * 0.35) h); --scale-300: oklch(from var(--base) 0.7 calc(c * 0.65) h); --scale-400: oklch(from var(--base) l c h); --scale-500: oklch(from var(--base) 0.35 calc(c * 0.8) h); --scale-600: oklch(from var(--base) 0.2 calc(c * 0.5) h); --scale-700: oklch(from var(--base) 0.1 calc(c * 0.2) h);
This is genuinely revolutionary for design systems. Instead of maintaining hundreds of manually chosen color tokens, you can define a handful of base colors and derive the rest mathematically. Changes propagate automatically. Want to shift your brand's entire palette by 15 degrees? Change one hue value. Need to make all backgrounds lighter? Adjust one lightness calculation.
Modern displays β from iPhones and MacBooks to high-end Android devices and PC monitors β support wide color gamuts like Display P3. In 2026, over 80% of consumer devices can display colors beyond sRGB. The color() function in CSS lets you target these gamuts directly:
/* Display P3 colors in CSS */ .p3-red { color: color(display-p3 1 0.2 0.1); } /* A vibrant red that can't be reproduced in sRGB */ .p3-green { color: color(display-p3 0.2 0.9 0.3); } /* A vivid green far outside sRGB gamut */ .p3-blue { color: color(display-p3 0.15 0.3 0.95); } /* A pure saturated blue impossible in sRGB */ /* Combining OKLCH and wide gamut */ .vibrant { color: oklch(0.6 0.35 140); } /* OKLCH automatically uses the widest available gamut */
You can also use oklch() to express wide-gamut colors without specifying a gamut explicitly β OKLCH is device-independent and will map to the display's native gamut. If you need to stay within sRGB for older displays, include an sRGB fallback:
/* Wide gamut with sRGB fallback */ .hero-accent { color: #ff4a3a; color: color(display-p3 1 0.25 0.15); } /* Using @supports for progressive enhancement */ @supports (color: color(display-p3 0 0 0)) { .hero-accent { color: color(display-p3 1 0.25 0.15); } }
Let's put everything together into a practical design system foundation. Here's how to build a scalable, maintainable color system using OKLCH and relative colors:
:root { /* Base brand colors β single source of truth */ --brand-hue: 270; /* Purple base */ --brand-chroma: 0.2; --brand-lightness: 0.55; /* Primary color and its scale */ --primary: oklch(var(--brand-lightness) var(--brand-chroma) var(--brand-hue)); --primary-100: oklch(0.95 calc(var(--brand-chroma) * 0.1) var(--brand-hue)); --primary-200: oklch(0.85 calc(var(--brand-chroma) * 0.3) var(--brand-hue)); --primary-300: oklch(0.7 calc(var(--brand-chroma) * 0.6) var(--brand-hue)); --primary-400: var(--primary); --primary-500: oklch(0.4 calc(var(--brand-chroma) * 0.85) var(--brand-hue)); --primary-600: oklch(0.25 calc(var(--brand-chroma) * 0.6) var(--brand-hue)); --primary-700: oklch(0.12 calc(var(--brand-chroma) * 0.3) var(--brand-hue)); /* Neutral scale β all gray, all perceptual */ --neutral-100: oklch(0.97 0 0); --neutral-200: oklch(0.92 0 0); --neutral-300: oklch(0.85 0 0); --neutral-400: oklch(0.7 0 0); --neutral-500: oklch(0.55 0 0); --neutral-600: oklch(0.4 0 0); --neutral-700: oklch(0.25 0 0); --neutral-800: oklch(0.15 0 0); --neutral-900: oklch(0.08 0 0); /* Semantic colors derived from brand */ --success: oklch(0.55 0.18 150); --warning: oklch(0.7 0.15 85); --danger: oklch(0.55 0.2 25); --info: oklch(0.6 0.12 220); } /* Dark theme β just override lightness */ [data-theme="dark"] { --primary-100: oklch(0.35 calc(var(--brand-chroma) * 0.6) var(--brand-hue)); --primary-200: oklch(0.45 calc(var(--brand-chroma) * 0.7) var(--brand-hue)); /* ... swap the scale direction */ }
With this approach, you can rebrand an entire site by changing just the --brand-hue custom property. The entire color scale, all derived values, and semantic assignments will follow. No more hand-selecting color scales for every new project.
As of May 2026, browser support for modern CSS color features is excellent:
| Feature | Support | Notes |
|---|---|---|
oklch() |
β 87%+ | Chrome 111+, Safari 15.4+, Firefox 113+, Edge 111+ |
color-mix() |
β 84%+ | Chrome 111+, Safari 16.2+, Firefox 113+, Edge 111+ |
| Relative Colors | β 82%+ | Chrome 119+, Safari 16.2+, Firefox 129+, Edge 119+ |
color(display-p3) |
β 72%+ | Chrome 111+, Safari 15.4+, limited Firefox |
For production use, provide simple fallbacks. CSS's cascade makes this straightforward β older browsers get the sRGB-safe value, modern browsers use the advanced color:
/* Practical fallback pattern */ .element { /* sRGB fallback for any browser */ background: #6c5ce7; color: #1a1a2e; /* Modern OKLCH overrides for supporting browsers */ background: oklch(0.55 0.2 270); color: oklch(0.15 0 0); } /* Optional: feature query for OKLCH */ @supports (color: oklch(0.5 0.1 0)) { .element { background: oklch(0.55 0.2 270); } }
calc() inside relative color syntax. Test your most critical paths. As of Safari 18 (late 2025), all features are stable across the latest versions of all major browsers.
Working with OKLCH effectively requires good tools. Here are the best resources in 2026:
postcss-preset-env to let PostCSS compile OKLCH back to sRGB fallbacks automatically during your build process.The developer tooling landscape for OKLCH has matured significantly. VS Code extensions for color preview now support OKLCH, design tools like Figma can export OKLCH values, and CSS frameworks like Tailwind CSS have added OKLCH-based color palettes to their configuration system.
Modern CSS color features β OKLCH, color-mix(), relative color syntax, and native wide-gamut support β represent the most significant upgrade to web color since CSS 3 introduced rgba() and hsl() over a decade ago.
The key takeaways:
color-mix() β it's native, dynamic, and works with CSS custom propertiesThe future of CSS color is here. It's more powerful, more intuitive, and more beautiful than what we've been working with. Start exploring OKLCH today, and transform how you think about color on the web.
Ready to see OKLCH in action? Use ColorPick to pick any color on your screen and explore its OKLCH values. Build your color systems smarter in 2026.
β Back to Blog | ColorPick App | Β© 2026 ColorPick