Modern CSS Color Functions: OKLCH, color-mix() and More
A practical tour of CSS color functions, from rgb() and hsl() to oklch(), color(), color-mix() and relative colors, with browser support as of November 2026.
By ColorOverflow Editorial TeamUpdated November 13, 20266 min readIntermediate
For years, CSS colors meant HEX codes, rgb() and hsl(). Modern CSS adds several more functions: ones that match how people see lightness, ones that reach colors beyond the standard sRGB range, and ones that mix or adjust colors right in the stylesheet. You don't need all of them, but a few can save a lot of manual work.
This guide walks through each function with a short example, explains why OKLCH has become popular for color systems, and lists browser support as of November 2026. If you're new to color codes, start with our guide to HEX, RGB and HSL.
Read the full guideHEX vs RGB vs HSL: Color Codes Explainedrgb() and hsl(), in modern syntax
Both functions now take space-separated values with an optional alpha after a slash, such as rgb(99 102 241 / 80%) and hsl(239 84% 67% / 80%). The comma form and the rgba() and hsla() names still work, so old stylesheets keep working. The modern syntax has been Baseline since January 2020.
hwb(): hue, whiteness and blackness
hwb() starts from a pure hue and adds white and black, a bit like mixing paint. hwb(239 39% 5%) is roughly the indigo #6366F1: hue 239, about 39% white and 5% black. It's easy to reason about for tints and shades, but it shares HSL's problem that equal numbers don't look equally light. It has been Baseline since 2022.
lab() and lch(): built around perception
lab() and lch() use the CIELAB color space, designed so that a lightness value matches how light a color looks. lab() takes a lightness from 0 to 100 and two axes, a (green to red) and b (blue to yellow). lch() takes the same lightness plus chroma (how colorful) and a hue angle, which is easier to edit by hand.
CIELAB has a known flaw: when you change only the lightness or chroma of some blues, the hue drifts toward purple. That flaw is what OKLab was made to fix.
oklab() and oklch(): the modern choice
OKLab is a newer perceptual color space, and oklch() is its lightness, chroma and hue form. Lightness runs from 0% to 100%, chroma from 0 (gray) up to about 0.4 for the most vivid colors, and hue is an angle. The indigo #6366F1 is oklch(58.54% 0.2041 277.12).
The big benefit is predictable lightness. Four colors at oklch(70% 0.12 …) with hues of 90, 150, 260 and 330 come out as #BB9B39, #63B376, #729FE9 and #C882C2. Against black they score 7.86:1, 8.24:1, 7.85:1 and 7.42:1, close to each other because they really are about equally light.
- #BB9B39
- #63B376
- #729FE9
- #C882C2
oklch(70% 0.12) at hues 90, 150, 260 and 330. They look about equally light, and their contrast with black stays between 7.42:1 and 8.24:1.
Compare that with HSL. Yellow, green, blue and magenta at hsl(… 100% 50%) share one lightness number, yet their contrast with black is 19.55:1, 15.30:1, 2.44:1 and 6.69:1. Same number, wildly different results.
Even lightness scales
Because OKLCH lightness is reliable, it's a good space for building a scale of one color. Keep the hue fixed, step the lightness down evenly, and adjust the chroma so the lightest and darkest steps don't become garish. Every step of the scale below keeps hue 277.
- #F1F4FF
- #D5DCFF
- #AFB9F8
- #8A96EE
- #6971E3
- #4E4ECC
- #3733A0
- #222168
Lightness 97%, 90%, 80%, 70%, 60%, 50%, 40% and 30%. White text first passes 4.5:1 at the 50% step (6.39:1); the 60% step reaches only 4.14:1.
color() and wide-gamut colors
HEX, rgb() and hsl() can only describe sRGB, the color range of a standard screen. Many phones and laptops now show the wider Display P3 range, which includes more vivid reds, greens and oranges. The color() function names a color space explicitly, such as color(display-p3 1 0 0.5), and oklch() can also reach colors outside sRGB when you raise the chroma.
When a screen can't show a color, the browser maps it to the closest color it can show. To control the fallback yourself, write an sRGB value first and the wide-gamut value second, or use the color-gamut media query.
CSS
.accent {
background: rgb(255 0 128); /* every browser */
background: color(display-p3 1 0 0.5); /* wider gamut where supported */
}
@media (color-gamut: p3) {
.hero { background: color(display-p3 0.95 0.2 0.4); }
}color-mix(): blend two colors
color-mix() blends two colors in a color space you choose. color-mix(in srgb, #6366F1 50%, white) gives #B1B3F8, a light indigo. Mixing with transparent instead lowers the alpha, which is handy for tinted backgrounds made from a brand variable.
CSS
:root { --brand: #6366f1; }
.badge {
background: color-mix(in oklab, var(--brand) 15%, white);
border-color: color-mix(in srgb, var(--brand) 40%, transparent);
}The color space changes the result. Mixing in srgb averages the red, green and blue values, which can dull the result; mixing in oklab or oklch usually keeps it more vivid and its lightness more even. Pure blue and pure yellow mixed half and half in srgb give the flat gray #808080, while in oklch the result is a saturated teal, because oklch travels around the hue wheel.
Relative color syntax: adjust a color
Relative color syntax takes an existing color, breaks it into channels and lets you change any of them. The keyword from names the source color, and the channel letters stand for its values.
CSS
:root { --brand: #6366f1; }
.button:hover {
/* 0.1 lower OKLCH lightness, same chroma and hue */
background: oklch(from var(--brand) calc(l - 0.1) c h);
}
.focus-ring {
/* the brand color at 40% alpha */
outline-color: rgb(from var(--brand) r g b / 40%);
}This removes a lot of hand-made variants: one brand variable can produce hover, pressed, border and background colors that update automatically when the brand changes.
Named colors and currentColor
CSS has nearly 150 named colors, from red and navy to rebeccapurple and lightgoldenrodyellow. They're fine for quick tests and demos, but few of them suit a real palette, so most design systems use their own values.
currentColor is different and very useful. It means the element's current text color, so an icon set to fill: currentColor or a border set to currentColor follows the text automatically, including on hover and in dark mode.
Browser support as of November 2026
- Space-separated rgb() and hsl() with slash alpha: Baseline since January 2020, safe everywhere.
- hwb(): Baseline since April 2022, safe everywhere.
- lab(), lch(), oklab(), oklch(), color() and color-mix(): Baseline since May 2023 and widely available since November 2025, so safe for nearly all visitors.
- Relative color syntax: Baseline since September 2024 (Chrome and Edge 125, Firefox 128, Safari 18). Supported in every current browser, but add a plain fallback if you support older devices.
- color-mix() with three or more colors: not yet in every browser, so stick to two.
- Named colors and currentColor: supported in every browser for many years.
Support data comes from the Baseline status that MDN and web.dev publish. For newer features, the safest pattern is the same as for wide gamut: declare a simple fallback first and the modern value second.
Which function should you use?
- HEX for storing and sharing exact colors, such as brand values and palette files.
- oklch() for building scales and themes, where even lightness matters.
- color-mix() and relative colors for derived states, such as hover, tints and translucent borders, generated from one variable.
- color(display-p3 …) or high-chroma oklch() for the occasional extra-vivid accent, always with an sRGB fallback.
- currentColor for icons and borders that should follow the text.
Whatever you write, the browser ends up with a color on screen, and that's what readers see. Build with whichever function makes the job easiest, then check the final colors for contrast.
Shades & TintsTurn one color into a full scale of lighter and darker versions.