CSS Color Module Level 4: Every New Function (2026)
CSS Color Module Level 4 added oklch, oklab, lab, lch, hwb and color(). Here is what each function does, the exact value ranges, and 2026 browser support.
CSS Color Module Level 4: Every New Function
For twenty-five years CSS could only describe colors inside sRGB. CSS Color Module Level 4 changed that: it added lab(), lch(), oklab(), oklch(), hwb() and color(), plus the rules browsers use to interpolate and gamut-map between them. As of November 2025 the whole set is Baseline widely available. This guide covers each function, its exact value ranges, and what belongs to Level 5 instead.
TL;DR
- Level 4 adds
lab(),lch(),oklab(),oklch(),hwb()andcolor()to CSS. color-mix(), relative color syntax andlight-dark()are Level 5, not Level 4.- All six Level 4 functions hit Baseline widely available on 9 November 2025.
- In
oklch(),100%chroma means0.4— not1. Common source of bugs. - Prefer
oklch()overlch(): LCH shifts blues toward purple.
What is CSS Color Module Level 4?
CSS Color Module Level 4 is the W3C spec that rebuilt CSS color from the ground up. It formalised the modern space-separated syntax (rgb(255 0 0 / 0.5) instead of rgba(255, 0, 0, 0.5)), added device-independent color spaces, and — crucially — specified what a browser must do when you write a color it cannot display.
Which functions are actually in Level 4
Six new color notations, plus the modern syntax for the legacy ones:
lab()andlch()— CIE Lab and its polar form, D50 whitepointoklab()andoklch()— the improved Lab model, D65 whitepointhwb()— hue, whiteness, blackness; sRGB onlycolor()— an escape hatch into named color spaces like Display P3noneas a component value, creating "missing components" for interpolation
What Level 5 adds on top
This is where most articles get it wrong. color-mix() is not in Level 4. It lives in CSS Color Module Level 5, a delta spec that also defines relative color syntax, light-dark(), device-cmyk() and custom color profiles. A newer Level 6 Editor's Draft now hosts contrast-color() and color-layers(), and is explicitly marked "not ready for implementation."
Where the spec stands in 2026
Level 4 has been a Candidate Recommendation since 5 July 2022, with the current Candidate Recommendation Draft dated 28 July 2026 — the CSS Working Group has published more than a dozen drafts in 2026 alone. Level 5 is still a Working Draft. Neither is a finished Recommendation, and that no longer matters much: interoperability, not REC status, is what decides whether you can ship.
How to use oklch() and oklab() in CSS
oklch() describes a color as lightness, chroma and hue. oklab() describes the same color as lightness plus two Cartesian axes. Both sit on Björn Ottosson's Oklab model, published in December 2020 and cited normatively by the CSS spec.
:root {
/* L = 0–1, C ≈ 0–0.4, H = 0–360deg */
--brand: oklch(0.62 0.19 264);
--brand-soft: oklch(0.62 0.19 264 / 0.15);
/* identical color, Cartesian form */
--brand-alt: oklab(0.62 -0.02 -0.19);
}
The channel ranges that trip everyone up
Percentages do not behave uniformly across these functions. MDN calls this out directly on the oklch() page: 100% equals 0.4 for chroma, but 1 for lightness. The same asymmetry runs through the whole family.
| Function | Channel | 100% equals |
|---|---|---|
oklch() |
L / C | 1 / 0.4 |
oklab() |
L / a, b | 1 / 0.4 |
lch() |
L / C | 100 / 150 |
lab() |
L / a, b | 100 / 125 |
So oklch(70% 100% 250) is oklch(0.7 0.4 250) — a wildly saturated color, not a mildly saturated one. Pick one form per codebase and stay with it. The iKit Color Picker shows the number form alongside HEX and RGB, which is the fastest way to sanity-check a value you inherited.
Why does lch() blue look purple
Hold lightness and hue fixed in lch(), raise chroma, and blues drift toward purple. Chrome's color-spaces guide pins the affected range: hue values between 270deg and 330deg shift chroma and lightness in ways the author did not ask for. The CSS Color 4 spec's own figure captions describe a "noticeable purpling" in the LCH slice around primary blue, and a constant visual hue in the OkLCh slice.
That single difference is why design systems generate tints and shades in OkLCh: the operation you perform most often — vary chroma, keep hue — is the operation LCH handles worst.
When to reach for oklab instead
Use oklch() for authoring, oklab() for math. Anything that averages or blends colors is cleaner in the Cartesian form because there is no hue angle to wrap around 360°. That is also why Oklab is the default interpolation space for CSS gradients and mixes.
How to use the color() function for Display P3
color() takes a color space name followed by three coordinates, each 0–1 across the space's bounds:
.vivid {
/* sRGB fallback first — dropped if unparsed */
background: #0a84ff;
background: color(display-p3 0.04 0.52 1);
}
Every predefined color space in color()
Ten are defined: srgb, srgb-linear, display-p3, display-p3-linear, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50 and xyz-d65. Plain xyz is a synonym for xyz-d65. The RGB-style spaces take r g b channel keywords in relative syntax; the XYZ ones take x y z, and mixing the two sets is a parse error.
When to use display-p3 instead of sRGB
Use it when the saturation you want does not exist in sRGB — brand accents, data-viz categoricals, product photography overlays. Chrome's high-definition color guide frames it as roughly 50% more colors available on capable displays. Two caveats worth knowing:
- On an sRGB screen the browser gamut-maps the color down, so you get something sensible, not a broken render.
@supportstells you the syntax parses.@media (color-gamut: p3)tells you the display can actually show it. They answer different questions.
How does color-mix() work in CSS
color-mix() blends two colors in a named interpolation space. It is the highest-leverage function in the whole family because it turns one brand token into an entire scale.
:root { --brand: oklch(0.62 0.19 264); }
.card { border: 1px solid
color-mix(in oklab, var(--brand) 30%, white); }
.card:hover { background:
color-mix(in oklab, var(--brand) 12%, white); }
.card:active { background:
color-mix(in oklab, var(--brand) 20%, black); }
Why percentages that don't add to 100 change alpha
This is the behaviour that surprises people. The spec normalises percentages, then uses whatever is missing to scale alpha. Given color-mix(in srgb, red 20%, green 60%), the sum is 80%, so the ratios rescale to 25%/75% and the result's alpha is multiplied by 0.8. Go over 100% instead and the percentages simply scale down with no alpha effect — purple 80%, plum 80% is just a 50/50 mix.
Omit one percentage and it becomes 100% minus the other. Omit both and you get 50/50. Negative percentages are invalid.
Interpolation happens on premultiplied alpha, which matters more than it sounds: the spec's own worked example shows the correct and naive results differing by ΔE2000 = 30.7 — a difference nobody would call subtle.
Hue interpolation: shorter, longer, increasing, decreasing
In polar spaces (hsl, hwb, lch, oklch) you can append a hue interpolation method. The default is shorter, which takes the short way around the wheel; longer takes the long way; increasing and decreasing always go clockwise or counterclockwise regardless of distance.
/* short arc — muddy through grey */
background: linear-gradient(
in oklch shorter hue, yellow, blue);
/* long arc — sweeps the full rainbow */
background: linear-gradient(
in oklch longer hue, yellow, blue);
The practical reason to use increasing/decreasing is animation: they do not flip sides when the hue difference crosses 180°, so a rotating hue stays smooth. The iKit Gradient Generator previews all four against the same stops if you would rather see it than reason about it.
Relative color syntax and the rest of Level 5
Relative color syntax lets a function read channels off an origin color:
:root { --base: oklch(52.6% 0.115 44.6deg); }
.summary { background:
oklch(from var(--base) l c calc(h + 90)); }
/* → oklch(0.526 0.115 134.6) */
Why channel keywords are always unitless numbers
l, c, h and friends resolve to plain <number> values. A percentage origin resolves to its number form; an angle resolves to a number of degrees in [0, 360] no matter whether it was authored in deg, rad or turn. Safari 16.4 shipped an older revision where units survived, which is why you still see calc(h + 180deg) guarded behind @supports in older codebases.
Relative syntax also only works with modern color syntax — the origin can be legacy rgb(255, 0, 0), but the outer function cannot use commas.
light-dark() and contrast-color()
light-dark() picks between two values based on the used color-scheme, so it only does anything if you set color-scheme: light dark on the root. It has been Baseline newly available since 13 May 2024. contrast-color() — which auto-selects a readable foreground — is newer still: Safari 26 shipped it in September 2025, Chrome and Edge 147 in April 2026. Its keyword syntax is still an open CSSWG issue, so treat any code sample you find as provisional and check contrast yourself against WCAG thresholds.
Browser support and fallbacks in 2026
Baseline status at a glance
| Feature | Baseline | Since |
|---|---|---|
oklch() / oklab() |
Widely available | 2025-11-09 |
lab() / lch() |
Widely available | 2025-11-09 |
color() |
Widely available | 2025-11-09 |
color-mix() |
Widely available | 2025-11-09 |
hwb() |
Widely available | 2024-10-28 |
| Relative color syntax | Newly available | 2024-09-16 |
light-dark() |
Newly available | 2024-05-13 |
The first four crossed into "widely available" on the same day — nine months ago at the time of writing. That is the fact that changes how you should write CSS today: oklch() and color-mix() no longer need a story, they need a value.
@supports patterns that actually work
For the widely-available functions, the cascade is enough. Declare sRGB first; an unparseable declaration is dropped and the earlier one wins:
.btn {
background: #4499bb;
background: oklch(0.65 0.1 240);
}
For relative color syntax — the one feature here still short of widely available — a real feature query earns its keep:
@supports (color: hsl(from white h s l)) {
.btn { background:
oklch(from var(--brand) calc(l * 0.9) c h); }
}
If you are generating a full scale rather than one-off adjustments, doing it once in a tool and exporting fixed tokens sidesteps the support question entirely; the iKit Color Palette Generator builds OkLCh ramps in the browser and hands back plain values.
References
- CSS Color Module Level 4 — W3C Candidate Recommendation Draft, 28 July 2026; source for the function list, D50/D65 whitepoints, and the LCH-vs-OkLCh blue figures.
- CSS Color Module Level 5 — W3C Working Draft, 18 June 2026; source for
color-mix()percentage normalization, relative color syntax rules, andlight-dark(). - oklch() CSS function - CSS | MDN — exact L/C/H ranges and the
100% = 0.4chroma mapping. - color() CSS function - MDN Web Docs — the full list of predefined color spaces and channel-keyword rules.
- Access more colors and new spaces | Chrome for Developers — the 270deg–330deg LCH hue-shift range and Display P3 framing.
- A perceptual color space for image processing — Björn Ottosson, December 2020; the primary Oklab source cited by the CSS spec.
Related on iKit
- A practical guide to using oklch() in CSS — the deep dive on the single most useful function in Level 4, with syntax and fallbacks.
- Why design systems are switching from HSL to OKLCH — the perceptual-uniformity argument that motivates most of Level 4.
- How to build a perceptually uniform color ramp in OKLCH — what to do with
oklch()once you have picked a base hue. - How WCAG AA and AAA contrast ratios are calculated — the math behind
contrast-color(), and how to verify it yourself. - HEX to RGB: how the conversion actually works — for the sRGB fallback line that sits above every modern color declaration.
- RGB to HEX: the 2026 guide for designers and developers — the return trip, when a tool hands you
rgb()and your tokens want hex. - EyeDropper API: sample any pixel on screen — how to grab a color off the screen before converting it into an OkLCh token.
Related posts
EyeDropper API: Sample Any Pixel on Screen in 2026
How the EyeDropper API lets a web app sample any pixel on the screen, the exact JavaScript to call it, and the fallback you still need in 2026.
WCAG Contrast Ratios: How AA and AAA Are Calculated (2026)
A developer's guide to the WCAG contrast ratio formula, the 4.5:1 AA and 7:1 AAA thresholds, large-text exceptions, and how to check color contrast in code.
OKLCH in CSS: A Practical Guide for Developers (2026)
OKLCH in CSS gives you perceptually uniform color, wider P3 gamut, and predictable lightness. Here is the syntax, the math, and the fallbacks for 2026.