The Problem With sRGB Color Spaces

For years, CSS color has been locked to the sRGB gamut. That’s true whether you’re writing rgb(), hsl(), or hex values — they all map coordinates onto the same range of colors. That was fine when screens couldn’t display anything beyond sRGB, but hardware has moved on. Displays that support wider color ranges like Display P3 are now common, and the old CSS color functions simply cannot reach those colors.

There’s another issue that’s less obvious but just as important: the way sRGB spaces handle lightness and saturation produces muddy, greyish results. HSL, for instance, gives every hue the same saturation range of 0% to 100%. In practice, that means some colors get greyed out just to stay within that uniform range. Gradients between two saturated colors will often pass through a dull grey zone instead of taking a vivid path around the color wheel.

See the Pen [HSL "Dead Grey Zone" [forked]](https://codepen.io/smashingmag/pen/vYQzrad) by Geoff Graham.

See the Pen HSL "Dead Grey Zone" [forked] by Geoff Graham.

The grey problem also distorts perceived lightness. Two colors in HSL can share the same lightness value while looking clearly different in brightness — a side effect of how sRGB spaces treat saturation. The CSS Color Module Level 4 specification addresses these limitations by introducing new color spaces purpose-built for wider gamuts and more consistent results.

Gamuts and Color Spaces: Two Different Things

Understanding Oklch starts with sorting out two concepts that are easy to conflate. A gamut is simply a range — here, a range of colors. sRGB and Display P3 are both examples of color gamuts, with Display P3 offering a noticeably wider range.

A color space, on the other hand, defines the coordinate system used to plot points within a gamut. Think of the gamut as the container and the color space as the map for navigating it. Traditional CSS functions like hex(), rgb(), and hsl() are all color spaces that navigate the same sRGB gamut, which is why they’re limited to the same range of colors.

The new color spaces in the Level 4 spec — LAB, LCH, Oklab, and Oklch — were designed to work with wider gamuts like Display P3. Among them, Oklch stands out because its coordinates map intuitively to how we already think about color while avoiding many of the pitfalls of earlier spaces.

Lightness, Chroma, Hue

Oklch is easy to pick up if you know HSL. Instead of hue, saturation, and lightness, Oklch uses hue, chroma, and lightness. Hue and lightness work much the same as they do in HSL. Chroma is slightly different from saturation: saturation measures color intensity relative to the same hue’s maximum possible intensity, while chroma is an absolute measure of colorfulness. That distinction matters because it allows Oklch to represent colors that sRGB spaces simply cannot.

See the Pen [OKLCH Lightness [forked]](https://codepen.io/smashingmag/pen/NWELzEm) by Geoff Graham.

See the Pen OKLCH Lightness [forked] by Geoff Graham.

More importantly, Oklch measures lightness consistently regardless of chroma or hue. The classic side-by-side comparison — two colors with the same HSL lightness value appearing visibly different — disappears when you switch to Oklch. Colors are both vivid and uniform in perceived brightness.

Why Oklch Instead of LCH or LAB?

The earlier LAB and LCH spaces solved the grey dead zone problem but introduced a new one: hue could shift as other values changed. The perceived color would subtly change even when the hue coordinate stayed the same.

Oklab and Oklch were designed to fix that inconsistency. Black stays black, and hues stay put as chroma and lightness are adjusted. That makes Oklch the more reliable choice for CSS gradients and color manipulations where consistency matters. LCH and LAB are usable, but oklch() and oklab() produce fewer surprises.

Oklch also wins over Oklab on comprehensibility. While Oklab uses Cartesian coordinates, Oklch gives you lightness, chroma, and hue — the same intuitive model that made HSL popular, but with the wider gamut and consistency that sRGB spaces lack.

The New Color Space Syntax

Using Oklch in CSS is straightforward. The function signature is familiar to anyone who has written HSL:

oklch() takes a lightness value as a percentage, a chroma value, and a hue value in degrees. You can also add an optional alpha channel.

Notably, colors defined with oklch() are not limited to the sRGB gamut. If the display hardware supports it, the browser renders colors from the wider Display P3 range. This gives you access to colors that hex and rgb() can’t produce.

What It Means for Existing CSS

You aren’t forced to replace your existing hex values or rgb() calls. sRGB spaces remain fully supported and continue to work for the colors they can produce. But when you need colors beyond the sRGB gamut, or you want gradients that avoid grey dead zones, Oklch is the space most likely to behave as you’d expect.

Gradients between colors in Oklch can require care, though. Since the gamut is not a simple cube, a linear path between two colors may push outside the available range and cause clipping. In those cases, interpolating in a different space — or picking slightly different endpoint colors — can produce better results. The issue is case-specific, so it pays to test any critical gradients in the space you plan to ship.

Using oklch() In CSS Today

Browser support for oklch() is solid across all major engines, and the remaining gaps are closing quickly.

Oklch Caniuse
Image source: Caniuse. Information was retrieved on May 18, 2023. (Large preview)

Adopting the function doesn’t require a mental overhaul if you already know HSL, but there is one new value to learn: chroma. The syntax is familiar in spirit:

  • l: Lightness, expressed as a percentage from 0% (black) to 100% (white).
  • c: Chroma, a unitless number typically between 0 and 0.37.
  • h: Hue, in degrees from 0deg to 360deg.

Chroma is the value that separates OKLCH from HSL, and understanding it is the key to working with the function comfortably. It’s often conflated with saturation, but the two are distinct concepts:

“Chroma refers to the purity of a color. A hue with high chroma has no black, white, or gray added to it. Conversely, adding white, black, or gray reduces its chroma. It’s similar to saturation but not quite the same.”

The upper bound of 0.37 is not a hard spec limit. As Andrey Sitnik and Travis Turner explain:

“[Chroma] goes from 0 (gray) to infinity. In practice, there is actually a limit, but it depends on a screen’s color gamut (P3 colors will have bigger values than sRGB), and each hue has a different maximum chroma. For both P3 and sRGB, the value will always be below 0.37.”

The oklch() Syntax And Its Modern Niceties

The formal grammar from the CSS Color Module Level 4 spec:

oklab() = oklab( [ <percentage> | <number> | none]
    [ <percentage> | <number> | none]
    [ <percentage> | <number> | none]
    [ / [<alpha-value> | none] ]? )

Written more plainly, the structure looks like this:

oklch( [ lightness ] [ chroma ] [ hue ] )

Each component takes a distinct unit:

oklch( [ lightness = <percentage> ] [ chroma <number> ] [ hue <degrees> ]  )

The permissible ranges for those units:

oklch( [ lightness = <percentage (0%-100%)> ] [ chroma <number> (0-0.37) ] [ hue <degrees> (0deg-360deg) ]  )

A concrete example:

color: oklch(70.9% 0.195 47.025);

Notice that there are no commas between arguments, and the hue doesn’t require a unit. This relaxed syntax, introduced in Color Module Level 4, applies to existing sRGB functions as well:

/* Old Syntax */
hsl(26.06deg, 99%, 51%)

/* New Syntax */
hsl(26.06 99% 51%)

Another improvement: alpha transparency no longer needs a separate function like oklcha(). Instead, an optional alpha value follows a slash:

/* Old Syntax */
hsla(26.06deg, 99%, 51%, .75)

/* New Syntax */
hsl(26.06 99% 51% / .75)

Building In A Fallback

Even with strong browser support, you may need a fallback for legacy browsers or displays that can’t reproduce P3 colors. The simplest approach is to declare an sRGB fallback first, letting the cascade handle the rest:

color: hsl(26.06 99% 51%);
color: oklch(70.9% 0.195 47.025);

More defensive patterns exist, such as feature detection with @supports:

.some-class {
  color: hsl(26.06 99% 51%);
}

@supports (oklch(100% 0 0)) {
  .some-class {
    color: oklch(70.9% 0.195 47.025);
  }
}

Or querying for Display P3 support at the media level:

.some-class {
  color: hsl(26.06 99% 51%);
}

@media (color-gamut: p3) {
  .some-class {
    color: oklch(70.9% 0.195 47.025);
  }
}

These approaches are more verbose, though, and letting the cascade pick the final value is often sufficient.

Polyfills For Older Tooling

If you need to support older browsers or non-supporting frameworks, two polyfills are worth knowing. The postcss-oklab-function PostCSS plugin preprocesses OKLCH values during your build step. Alternatively, color.js can perform conversions on the client side at runtime.

Why Make The Switch?

The arguments for moving to Oklch are practical more than sentimental. It unlocks access to a wider gamut of colors, interpolates between hues more smoothly with fewer unexpected gray bands, and its perceptual uniformity means that equal value changes look like equal visual changes. Combined with broad browser support and straightforward fallback options, there’s little technical reason to keep reaching for HSL first.