Access more colors and new spaces

This document is part of the high-definition CSS color guide.

CSS Color 4 outlines a bunch of new features and tools for CSS and color. The following Codepen shows all the new and old color syntaxes together:

Read the recap of classic color spaces.

The level 4 specification introduced 12 new color spaces for looking up colors, up from the 7 new gamuts shared previously:

Meet the new web color spaces

The following color spaces offer access to larger gamuts than sRGB. The display-p3 color space offers almost twice as many colors as RGB, while Rec2020 offers almost twice as many as display-p3. That's a lot of colors!

Five stacked triangles of varying color to help illustrate
  the relationship and size of each of the new color spaces.

The color() function

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

The new color() function can be used for any color space that specifies colors with R, G and B channels. color() takes a color space parameter first, then a series of channel values for RGB and optionally some alpha.

You'll find many of the new color spaces use this function because having specialized functions like rgb, srgb, hsl, hwb, etc, was growing to a long list, easier to have the colorspace be a parameter.

Pros

  • A normalized space for accessing color spaces that use RGB channels.
  • Can scale up to any wide gamut RGB based color space.

Cons

  • Doesn't work with HSL, HWB, LCH, okLCH, or okLAB
.valid-css-color-function-colors {
  --srgb: color(srgb 1 1 1);
  --srgb-linear: color(srgb-linear 100% 100% 100% / 50%);
  --display-p3: color(display-p3 1 1 1);
  --rec2020: color(rec2020 0 0 0);
  --a98-rgb: color(a98-rgb 1 1 1 / 25%);
  --prophoto: color(prophoto-rgb 0% 0% 0%);
  --xyz: color(xyz 1 1 1);
}

sRGB via color()

sRGB triangle is the only one fully opaque, to help visualize the size of the gamut.

This colorspace offers the same features as rgb(). It does additionally offer decimals between 0 and 1, used exactly like 0% to 100%.

Pros

  • Nearly all displays support the range of this color space.
  • Design tool support.

Cons

  • Not perceptually linear (like lch() is)
  • No wide gamut colors.
  • Gradients often go through a dead zone.
.valid-css-srgb-colors {
  --percents: color(srgb 34% 58% 73%);
  --decimals: color(srgb .34 .58 .73);

  --percents-with-opacity: color(srgb 34% 58% 73% / 50%);
  --decimals-with-opacity: color(srgb .34 .58 .73 / .5);

  /* empty */
  --empty-channels-black: color(srgb none none none);
  --empty-channels-black2: color(srgb);
}

Linear sRGB via color() {#linear-srgb}

sRGB triangle is the only one fully opaque, to help visualize the size of the gamut.

This linear alternative to RGB offers predictable channel intensity.

Pros

  • Direct access to RGB channels, handy for things like game engines or light shows.

Cons

  • Not perceptually linear.
  • Black and white are packed at the edges.
.valid-css-srgb-linear-colors {
  --percents: color(srgb-linear 34% 58% 73%);
  --decimals: color(srgb-linear .34 .58 .73);

  --percents-with-opacity: color(srgb-linear 34% 58% 73% / 50%);
  --decimals-with-opacity: color(srgb-linear .34 .58 .73 / .5);

  /* empty */
  --empty-channels-black: color(srgb-linear none none none);
  --empty-channels-black2: color(srgb-linear);
}

Gradients are discussed in detail later, but quickly it's meaningful to see a srgb and linear-srgb black to white gradient to illustrate their differences:

Two horizontal gradients shown in two rows for easy comparison. The sRGB gradient is smooth, as we've seen for many years. The sRGB-linear gradient though is very dark in the first 5% and very light at the last 10%, making the middle very light gray for a long time.

LCH

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

LCH introduces syntax to access colors outside the RGB gamut. It's also the first to make it very easy to create out-of-gamut color for a display. This is because any CIE space colors (lch, oklch, lab, oklab) can represent the entire human visible color spectrum.

This colorspace is modeled after human vision and offers syntax to specify any of those colors and more. The LCH channels are lightness, chroma and hue. Hue being an angle, like in HSL and HWB. Lightness is a value between 0 and 100. It's a special "perceptually linear," human-centric lightness. Chroma is similar to saturation; it can range from 0 to 230, but is technically unbounded.

Pros

  • Predictable color manipulation thanks to being perceptually linear, mostly (see oklch).
  • Uses familiar channels.
  • Often has vibrant gradients.

Cons

  • Easy to go out of gamut.
  • On rare occasions the gradient may need an adjustment middle point to prevent hue shift.
.valid-css-lch-colors {
  --percent-and-degrees: lch(58% 32 241deg);
  --just-the-degrees: lch(58 32 241deg);
  --minimal: lch(58 32 241);

  --percent-opacity: lch(58% 32 241 / 50%);
  --decimal-opacity: lch(58% 32 241 / .5);

  /* chromaless and hueless */
  --empty-channels-white: lch(100 none none);
  --empty-channels-black: lch(none none none);
}

LAB

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

Another color space made to access the CIE gamut, again with a perceptually linear lightness (L) dimension. The A and B in LAB represent the unique axes of human color vision: red-green, and blue-yellow. When A is given a positive value it adds red, and adds green when it's below 0. When B is given a positive number it adds yellow, where negative values are toward blue.

Pros

  • Perceptually consistent gradients.
  • High dynamic range.

Cons

  • Potential for hue shift.
  • Difficult to hand author or guess a color when reading values.
.valid-css-lab-colors {
  --percent-and-degrees: lab(58% -16 -30);
  --minimal: lab(58 -16 -30);

  --percent-opacity: lab(58% -16 -30 / 50%);
  --decimal-opacity: lab(58% -16 -30 / .5);

  /* chromaless and hueless */
  --empty-channels-white: lab(100 none none);
  --empty-channels-black: lab(none none none);
}

OKLCH

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.4.

Source

This color space is corrective to LCH. And like LCH, (L) continues to represent perceptually linear lightness, C for chroma and the H for hue.

This space feels familiar if you've worked with HSL or LCH. Pick an angle on the color wheel for H, choose a lightness or darkness amount by adjusting L, but then we have chroma instead of saturation. They're fairly identical except that adjustments to lightness and chroma tend to come in pairs, or else it can be easy to ask for high chroma colors that go outside of a target gamut.

Pros

  • No surprises when working with blue and purple hues.
  • Perceptually linear lightness.
  • Uses familiar channels.
  • High dynamic range.
  • Has a modern color picker - by Evil Martians.

Cons

  • Easy to go out of gamut.
  • New and relatively unexplored.
  • Few color pickers.
.valid-css-oklch-colors {
  --percent-and-degrees: oklch(64% .1 233deg);
  --just-the-degrees: oklch(64 .1 233deg);
  --minimal: oklch(64 .1 233);

  --percent-opacity: oklch(64% .1 233 / 50%);
  --decimal-opacity: oklch(64% .1 233 / .5);

  /* chromaless and hueless */
  --empty-channels-white: oklch(100 none none);
  --empty-channels-black: oklch(none none none);
}

OKLAB

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.4.

Source

This space is corrective to LAB. It's claimed as a space optimized for image processing quality also, which for us in CSS means gradients and color function manipulation quality.

Pros

  • Default space for animations and interpolations.
  • Perceptually linear lightness.
  • No hue shift like LAB.
  • Perceptually consistent gradients.

Cons

  • New and relatively unexplored.
  • Few color pickers.
.valid-css-oklab-colors {
  --percent-and-degrees: oklab(64% -.1 -.1);
  --minimal: oklab(64 -.1 -.1);

  --percent-opacity: oklab(64% -.1 -.1 / 50%);
  --decimal-opacity: oklab(64% -.1 -.1 / .5);

  /* chromaless and hueless */
  --empty-channels-white: oklab(100 none none);
  --empty-channels-black: oklab(none none none);
}

Display P3

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

Display P3 triangle is the only one fully opaque, to help
  visualize the size of the gamut. It looks like 2nd from the smallest.

The display P3 gamut and color space have become popular since Apple supported them since 2015 on their iMac. Apple also supported display-p3 in web pages via CSS since 2016, five years ahead of any other browser. If coming from sRGB, this is a great color space to begin working within as you move styles to a higher dynamic range.

Pros

  • Great support, considered the baseline for HDR displays.
  • 50% more colors than sRGB.
  • DevTools offer a great color picker.

Cons

  • Will eventually be surpassed by Rec2020 and CIE spaces.
.valid-css-display-p3-colors {
  --percents: color(display-p3 34% 58% 73%);
  --decimals: color(display-p3 .34 .58 .73);

  --percent-opacity: color(display-p3 34% 58% 73% / 50%);
  --decimal-opacity: color(display-p3 .34 .58 .73 / .5);

  /* chromaless and hueless */
  --empty-channels-black: color(display-p3 none none none);
  --empty-channels-black2: color(display-p3);
}

Rec2020

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

Rec2020 triangle is the only one fully opaque, to help
  visualize the size of the gamut. It looks like 2nd from the largest.

Rec2020 is part of the movement to UHDTV (ultra-high-definition television), providing a wide range of colors for use in 4k and 8k media. Rec2020 is another RGB based gamut, larger than display-p3, but not nearly as common amongst consumers as Display P3.

Pros

  • Ultra HD colors.

Cons

  • Not as common among consumers (yet).
  • Not commonly found in handhelds or tablets.
.valid-css-rec2020-colors {
  --percents: color(rec2020 34% 58% 73%);
  --decimals: color(rec2020 .34 .58 .73);

  --percent-opacity: color(rec2020 34% 58% 73% / 50%);
  --decimal-opacity: color(rec2020 .34 .58 .73 / .5);

  /* chromaless and hueless */
  --empty-channels-black: color(rec2020 none none none);
  --empty-channels-black2: color(rec2020);
}

A98 RGB {#a98-rgb}

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

A98 triangle is the only one fully opaque, to help
  visualize the size of the gamut. It looks like the middle sizes triangle.

Short for Adobe 1998 RGB, A98 RGB was created by Adobe to feature most of the colors achievable from CMYK printers. It offers more colors than sRGB, notably in the cyan and green hues.

Pros

  • Larger than the sRGB and Display P3 color spaces.

Cons

  • Not a common space worked within by digital designers.
  • Not many folks are porting palettes from CMYK.
.valid-css-a98-rgb-colors {
  --percents: color(a98-rgb 34% 58% 73%);
  --decimals: color(a98-rgb .34 .58 .73);

  --percent-opacity: color(a98-rgb 34% 58% 73% / 50%);
  --decimal-opacity: color(a98-rgb .34 .58 .73 / .5);

  /* chromaless and hueless */
  --empty-channels-black: color(a98-rgb none none none);
  --empty-channels-black2: color(a98-rgb);
}

ProPhoto RGB

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

ProPhoto triangle is the only one fully opaque, to help
  visualize the size of the gamut. It looks like the largest.

Created by Kodak, this wide gamut space uniquely offers ultra wide range primary colors and features minimal hue shifts when changing lightness. It also claims to cover 100% of real-world surface colors as documented by Michael Pointer in 1980.

Pros

  • Minimal hue shifts when changing lightness.
  • Vibrant primary colors.

Cons

  • Around 13% of its colors offered are imaginary, meaning they're not within the human visible spectrum.
.valid-css-prophoto-rgb-colors {
  --percents: color(prophoto-rgb 34% 58% 73%);
  --decimals: color(prophoto-rgb .34 .58 .73);

  --percent-opacity: color(prophoto-rgb 34% 58% 73% / 50%);
  --decimal-opacity: color(prophoto-rgb .34 .58 .73 / .5);

  /* chromaless and hueless */
  --empty-channels-black: color(prophoto-rgb none none none);
  --empty-channels-black2: color(prophoto-rgb);
}

XYZ, XYZ-d50, XYZ-d65

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

The CIE XYZ color space encompasses all colors that are visible to a person with average eyesight. This is why it is used as a standard reference for other color spaces. Y is luminance, X and Z are possible chromas within the given Y luminance.

The difference between d50 and d65 is the white point, where d50 uses the d50 white points and d65 uses the d65 white point.

Key-Term: White point is an attribute of a color space, it's where true white exists within the space. For electronic screens, D65 is the most common white point, and it's short for 6500 kelvin. It's important in color conversion that white points match so color temperature (warmness or coolness) aren't affected.

Pros

  • Linear-light access has handy use cases.
  • Great for physical color mixing.

Cons

  • Not perceptually linear like lch, oklch, lab and oklab are.
.valid-css-xyz-colors {
  --percents: color(xyz 22% 26% 53%);
  --decimals: color(xyz .22 .26 .53);

  --percent-opacity: color(xyz .22 .26 .53 / 50%);
  --decimal-opacity: color(xyz .22 .26 .53 / .5);

  /* chromaless and hueless */
  --empty-channels-black: color(xyz none none none);
  --empty-channels-black2: color(xyz);
}
.valid-css-xyz-d50-colors {
  --percents: color(xyz-d50 22% 26% 53%);
  --decimals: color(xyz-d50 .22 .26 .53);

  --percent-opacity: color(xyz-d50 .22 .26 .53 / 50%);
  --decimal-opacity: color(xyz-d50 .22 .26 .53 / .5);

  /* chromaless and hueless */
  --empty-channels-black: color(xyz-d50 none none none);
  --empty-channels-black2: color(xyz-d50);
}
.valid-css-xyz-d65-colors {
  --percents: color(xyz-d65 22% 26% 53%);
  --decimals: color(xyz-d65 .22 .26 .53);

  --percent-opacity: color(xyz-d65 .22 .26 .53 / 50%);
  --decimal-opacity: color(xyz-d65 .22 .26 .53 / .5);

  /* chromaless and hueless */
  --empty-channels-black: color(xyz-d65 none none none);
  --empty-channels-black2: color(xyz-d65);
}

Custom color spaces

The CSS Color 5 specification also has a path for teaching the browser a custom color space. This is an ICC profile that tells the browser how to resolve colors.

@color-profile --foo {
  src: url(path/to/custom.icc);
}

Once loaded, access colors from this custom profile with the color() function and specify the channel values for it.

.valid-css-color-from-a-custom-profile {
  background: color(--foo 1 0 0);
}

Color interpolation

Transitioning from one color to another is found in animation, gradients and color mixing. This transition is typically specified as a starting color and an ending color, where the browser is expected to interpolate between them. Interpolate in this case means to generate a series of in-between colors to create a smooth transition instead of an instant one.

With a gradient, the interpolation is a series of colors along a shape. With animation it's a series of colors over time.

@keyframes bg {
  0%, 100% {
    background: orange;
  }
  25% {
    background: magenta;
  }
  50% {
    background: lime;
  }
  75% {
    background: cyan;
  }
}

.circle {
  animation: bg 5s ease-in-out infinite;
}

With a gradient, the in-between colors are shown all at once:

What's new in color interpolation

With the addition of new gamuts and color spaces, there are new additional options for interpolation. Transitioning a color in hsl from blue to white results in something very different from sRGB.

.classic-gradient-in-srgb {
  background: linear-gradient(to right, blue, white);
}

.new-gradient-in-hsl {
  background: linear-gradient(in hsl to right, blue, white);
}

Then what happens if you transition from a color in one space to a color in a completely different space:

.gradient {
  /* oklab will be the common space */
  background: linear-gradient(to right, lch(29.6 131 301), hsl(330 100% 50%));
}

.lch {
  /* lch is specified */
  background: linear-gradient(in lch to right, lch(29.6 131 301), hsl(330 100% 50%));
}

Luckily for you, the Color 4 specification has instructions for the browsers on how to handle these cross color space interpolations. For .gradient, browsers notice the differentiating color spaces and use the default color space oklab.

You may think the browser would use lch as the color space, since that's the first color, but it doesn't. That's why I show a second comparison gradient .lch. The .lch gradient is a gradient from the lch color space.

Less banding thanks to 16-bit color

Before this color work, all colors were saved in one 32-bit integer to represent all four channels; red, green, blue and alpha. This is 8-bits per channel and 2^ 24 possible colors (ignoring alpha). 2 ^ 24 = 16,777,216, "millions of colors."

After this color work, four 16-bit floating point values, each channel has its own float instead of being lumped together. This is 64-bits of data total, resulting in many more than millions of colors.

This work is required to support HD color. This increases the amount of color information that can be stored, which has a nice side effect of meaning there's more colors for the browser to use in a gradient.

Gradient banding is when there aren't enough colors to create a smooth gradient and "strips" of color become visible. Banding is heavily mitigated with the upgrade to higher resolution color.

Six panels are shown, each with a varying amount of gradient banding
    and a little bit of information about compression and bit depth.
Image source

Control interpolation

The shortest distance between two points is always a straight line. With color interpolation, browsers take the short route by default. Consider a scenario where there are two points in an HSL color cylinder. A gradient acquires its color steps by traveling along the line between the two points.

linear-gradient(to right, #94e99c, #e06242)
A circular gradient with a line from green to red, straight
    through the circle, going through the white areas.
(mock demonstration)
Top-down view of an HSL cylinder with a line between the color stops

The above gradient line goes straight between the greenish color to the reddish color, passing through the center of the color space. While the above is great to help with initial understanding, it's not exactly what happens. Here is the gradient in the following Codepen, and it's clearly not white in the middle like the mock demonstration showed.

The middle area of the gradient has lost its vibrance though. This is because the most vibrant colors are at the edge of the color space shape, not in the center where the interpolation traveled near. This is commonly referred to as the "dead zone." There are a few ways to fix or work around this.

Specifying more gradient stops to avoid the dead zone

A technique for avoiding the dead zone today is to add additional color stops in the gradient that intentionally guide the interpolation to stay within the vibrant ranges of a color space. It is literally a work around, as the additional stops help it work around the dead zone.

There's a gradient tool created by Erik Kennedy that calculates additional color stops for you, to help you avoid the dead zone even in color spaces that tend to gravitate towards it. Using it, passing the same colors from the first example but changing the color interpolation to HSL, it produces this:

linear-gradient(90deg, #94e99c, #99e789, #ace67d, #c4e472, #e2e366, #e2bf5a, #e1934e, #e06242);
A circular gradient with a line curving around the middle with many
    gradient stops along the way, guiding it away from the center.
(mock demonstration)
Top down view of an HSL cylinder with a curved line featuring 9 color stops

With guided stop points, the interpolation is no longer a straight line, but appears to curve around the dead zone, helping maintain saturation, resulting in a much more vibrant gradient.

While the tool does a great job, what if you could have similar or greater control right from CSS?

Directing the color interpolation

In Color 4, the ability to control the hue interpolation strategy was added and is a new way around (:wink:) the dead zone. Think about a hue angle and consider a 2 stop gradient that only changes the angle, going hue shifting from 140deg to 240deg for example.

Shorter vs longer hue interpolation

The gradient defaults to take the shorter route it can unless you specify for it to take the longer route. Hue interpolation options direct the angle rotation, like telling someone to turn left instead of right (heh, Zoolander):

The same gradient circle visual as before, but this time there is an
  inner circle drawn that shows the long way vs the short way.

In the example of hue interpolation distances, the short path and the long path are simulated to illustrate the difference. The short distance has less hues between it because it's traveled through the least amount of distance possible, where the long distance has traveled over more hues.

Increasing vs decreasing hue interpolation

There are two more hue interpolation strategies in Color 4, but they are exclusive for cylindrical color spaces. Staying with the two colors from the previous examples, the visual now shows how increasing and decreasing works.

The above Codepen used ColorJS to demonstrate the expected result. The CSS you would write to achieve the same effect without a Javascript library would be:

.longer-hue-interpolation-in-css {
  background: linear-gradient(
    to right in hsl longer hue,
    hsl(180deg 100% 75%),
    hsl(240deg 100% 75%)
  );
}

.decreasing-hue-interpolation-in-css {
  background: linear-gradient(
    to right in hsl decreasing hue,
    hsl(180deg 100% 75%),
    hsl(240deg 100% 75%)
  );
}

To close out hue interpolation, here's a fun playground where you can change the hue between 2 color stops and see the effects of a hue interpolation choice as well as how color spaces change gradient results. The effects can be very different; consider this as four new tricks just went into your color toolbelt.

Gradients in different color spaces

Each color space, given its unique shape and color arrangement, results in a different gradient. In the following examples, look at how each color space handles that differently, especially at blue to white. Notice how many become purple in the middle; that's called a hue shift during interpolation.

Some gradients in these spaces are more vibrant than others or travel less through dead zones. Spaces like lab pack colors together in a way optimized for saturation, as opposed to spaces optimized for humans to write color in like hwb().

.hwb {
  background: linear-gradient(to right, hwb(250 10% 10%), hwb(150 10% 10%));
}
.lab {
  background: linear-gradient(to right, lab(30 59.4 -96), lab(80 -64 36.2));
}

The above demo, while subtle in the results, does show more consistent interpolation with lab. The syntax of lab isn't simple to read though, there's negative numbers that are very unfamiliar when coming from rgb or hsl. Good news, we can use hwb for a familiar syntax but ask for the gradient to be interpolated entirely within another color space, like oklab.

.hwb {
  background: linear-gradient(in hwb to right, hwb(250 10% 10%), hwb(150 10% 10%));
}
.lab {
  background: linear-gradient(in oklab to right, hwb(250 10% 10%), hwb(150 10% 10%));
}

This example uses the same colors in hwb but specifies the color space for interpolation to either hwb or oklab. hwb is a great colorspace for high vibrance but possible dead zones or bright spots (see the cyan hot spot in the top example). oklab is great for perceptually linear gradients that stay saturated. This feature is a lot of fun as you can try on a few different color spaces to see which gradient you like best.

Here's a Codepen experimenting with gradients and color spaces, mixing and matching strategies to explore the possibilities. Even a transition from black to white is different in each color space!

Gamut clamping

There exist scenarios where a color may ask for something outside of a gamut. Consider the following color:

rgb(300 255 255)

The maximum for a color channel in the rgb color space is 255, but here 300 was specified for red. What happens? Gamut clamping.

Clamping is when extra information is simply removed. 300 becomes 255 internally to the color engine. The color has now been clamped within its space.

Choosing a color space

Many folks, after learning about these color spaces and their effects, feel overwhelmed and want to know which "one" to choose. From my studies and experience, I don't see one color space as the single one for all my tasks. Each has moments when they produce the desired outcome.

If there was one best space, then there wouldn't be so many new spaces being introduced.

However, I can say that the CIE spaces—lab, oklab, lch and oklch—are my starting places. If the outcome of them isn't what I'm looking for, then I'll go test other spaces. For mixing colors and creating gradients, I agree with the default spec choice of oklab. For color systems and overall UI colors, I like oklch.

Here are a couple articles where folks have shared their updated color strategies given these new color spaces and features. For example, Andrey Sitnik has gone all in on oklch, maybe they'll convince you to do the same:

  1. OKLCH in CSS: why we moved from RGB and HSL by Andrey Sitnik
  2. Color Formats by Josh W. Comeau
  3. OK, OKLCH by Chris Coyier

Next steps

Now that you're familiar with the new color spaces and tools, you can migrate to HD color.

More vibrance, consistent manipulations and interpolations and overall deliver a more colorful experience to your users.

Read more color resources from the guide.