Dec 21

CSS Color Spaces and Relative Color Syntax

Learn how to define color palettes that are perceptually uniform using the new Oklch color space and the relative color syntax in CSS.

By Evan Sheehan

There are two new specs in the works for CSS that will change how we work with color on the web: CSS Color Module Level 4 (candidate recommendation) and CSS Color Module Level 5 (working draft). Both are still experimental and, as of December 2022, only Safari has an implementation.

We’re going to focus on three things found in these specs: the new syntax for color functions, one of the (many) new color spaces introduced by level 4, and the new relative color syntax from level 5. Combined, these three things are powerful tools for building accessible color palettes with CSS.

New Color Function Syntax

CSS Color Module Level 4 introduces a new syntax for color functions like rgb() and hsl(). The new syntax omits commas, relying on spaces to separate each of the channels for the color space. It also supports an optional alpha parameter, eliminating the need for additional color functions like rgba() and hsla(). The comma-separated forms are now referred to as “legacy syntax” by the spec.

/* Legacy syntax */
background-color: hsl(270, 50%, 40%);
color: hsla(0, 0%, 100%, 50%);

/* CSS Color Module Level 4 equivalent */
background-color: hsl(270 50% 40%);
color: hsl(0 0% 100% / 50%);

True to the web, existing functions like rgb() and hsl() will continue to support both the new syntax and the legacy, comma-separated syntax. Newer color functions — some of which we’ll look at below — only support the new, space-separated syntax.

New Color Spaces

The new color spec adds a whole host of new color spaces to the web: Hue, Whiteness, Blackness (HWB); CIE L*a*b*; Lightness, Chroma, Hue (LCH); Oklab; Oklch; and Display P3, just to name a few. Some of these color spaces — like Display P3 — offer a wider color gamut than the sRGB space we’ve been restricted to on the web since… well, forever. This means that we’ll have access to more colors and that those colors will be more vibrant than the colors we’ve been using, colors that have been available to native app developers for a while.

We’re not going to focus on the wider gamut spaces, though. If you’re interested in learning more about these wide gamut color spaces (or simply what even is a color gamut), I suggest watching Chris Lilley’s CSS Day 2022 talk, Escaping the sRGB Prison (YouTube).

We’re going to focus on Oklch. 

Oklch

Oklch is a polar color space, similar to HSL. It has three channels: lightness (l), chroma (c), and hue (h). Lightness can be either a number between 0 and 1, or a percentage. Hue can be either a number (without units, this corresponds to degrees) or an angle (using units deg, grad, rad, or turn). Chroma takes either a number (unbounded, but in practice doesn’t exceed 0.5) or a percentage (100% is equivalent to 0.4). Chroma is similar to saturation, but it is independent of the perceived lightness for a given hue.

Perceptual Uniformity

We’re focusing on Oklch because, unlike sRGB, Oklch is perceptually uniform. This means that uniform changes to the lightness channel of a color are evenly spaced visually and that two colors with the same lightness value will have the same perceived lightness — which is not true in a color space like HSL.

Two color swatches with the same HSL lightness, have very different perceived lightness, as evident when converted to Oklch.
Two color swatches with the same HSL lightness, have very different perceived lightness, as evident when converted to Oklch.

The above example — from the level 4 spec — shows a blue and yellow swatch in sRGB color space, specified by #00F and #FF0, respectively. In HSL, both colors have the same lightness value (50%), but yellow is clearly much lighter than blue. The difference in lightness is reflected in the Oklch color space where the lightness of the blue swatch (0.452) is much lower than the lightness of the yellow (0.968).

The above example makes clear that we can’t rely on the lightness channel in HSL to help us create color palettes with enough contrast to be accessible. But by using a perceptually uniform color space like Oklch, we will have a much easier time creating accessible palettes.

If we look at a gradient going from black to white, we can see the difference in lightness between HSL and Oklch. When interpolated through Oklch, each step creates a uniform increase in perceived lightness. When interpolated through HSL, the perceived lightness changes more quickly in darker values than it does in lighter values.

The same gradient interpolated through two different color spaces — HSL and Oklch — with the mid-gray value marked on each.
The same gradient interpolated through two different color spaces — HSL and Oklch — with the mid-gray value marked on each.

In the above gradients, notice how the darker shades are clustered closer to the left edge of the gradient for HSL than they are for Oklch. The point closest to a mid-gray is marked on each of the gradients. For Oklch, it is approximately in the middle of the gradient, but for HSL, it is slightly to the left of center.

Relative Color Syntax

CSS Color Module Level 5 further augments color functions by introducing relative color syntax. This syntax allows you to define a new color based on another color. You use it by first defining an origin color using the from keyword and then specifying the channels for the new color as usual in the color function.

Relative color syntax only works in the modern, space-separated color formats, not the legacy, comma-separated formats.

When you provide an origin color, you get access to “channel keywords” that allow you to reference each of the channels in the color space. The keywords change depending on the color function you use. For rgb(), you’d have the r, g, and b channel keywords; for oklch(), you’d have the l, c, and h keywords. For every color function, you also have an alpha channel keyword, that refers to the origin color’s alpha channel.

You can use these channel keywords in calc() expressions to modify the original color.

/* Desaturate the named color tomato in sRGB */
rgb(from tomato calc(r - 20) calc(g - 20) calc(b - 20));

/* Create a semi-transparent version of tomato */
rgb(from tomato r g b / 50%)

/* Darken tomato in oklch */
oklch(from tomato calc(l - 0.1) c h);

You can define relative colors across color spaces as well. When you take a color originally defined in one space and define a new color using a different color space, the browser will first convert the origin color to the new color space.

Here we use Oklch to define a secondary color based on a primary color defined in sRGB by rotating the hue 120° (⅓ of a turn):

--primary: #005f73;
--secondary: oklch(from var(--primary) l c calc(h + 120));

Relative Colors for Design Systems

Sometimes you’ll see people define colors in CSS using one custom property for each color channel — Josh Comeau recently wrote about this in his section on super-charged design tokens in his article about color formats. For example, let’s redefine our primary color — #005f73 — in separate l, c, and h channels so we can create shades in Oklch:

/* Define the channels for primary color in oklch */
--primary-l: 0.4485;
--primary-c: 0.081;
--primary-h: 218.73;

/* Base primary color */
--primary: oklch(var(--primary-l) var(--primary-c) var(--primary-h));

/* Create a darker shade of the primary color by subtracting 0.1 from the lightness channel */
--primary-darker: oklch(calc(var(--primary-l) - 0.1) var(--primary-c) var(--primary-h));

You can probably see where this is going…

The new relative color syntax removes the need to break each of your colors up into separate channels when you define them. It also allows you to use the most convenient color format for your origin colors regardless of the color space you want to use to modify the origin color.

If you’re working from a design tool that makes it easy to copy sRGB hex values, you can just paste those into your CSS and still use Oklch or Oklab to define perceptually uniform shades.

--primary: #005f73;
--primary-darker: oklch(from var(--primary) calc(l - 0.1) c h);

See how much more convenient that is?

And More…

There’s a lot more coming to colors in CSS. The level 5 spec also introduces a color-mix() function that allows you to mix colors in different color spaces — another potentially useful tool in the design system arsenal. And the editor’s draft for the CSS Images Module Level 4 introduces syntax to gradients for defining the interpolation space of the gradient, allowing you to create more vibrant gradients or achieve different artistic effects with your gradients. We glossed over color gamuts, but the new, wider gamut color spaces like Display P3 will give us access to brighter, more vibrant colors than we’ve had access to up until now.

Additional Resources

Evan selected The Trevor Project for an honorary donation of $50 which has been matched by Netlify

The Trevor Project

The Trevor Project’s mission is to end suicide among LGBTQ young people.