where color in the browser was before and is after CSS Color Module 4

Sep 26, 2023

for over 25 years, CSS was only able to specify colors within the sRGB gamut, through various methods like color names, hex codes and syntaxes such as rgb() , hsl() and hwb() .

but as consumer displays quickly became capable of showing wider ranges of colors (Display P3, Rec2020 etc.), websites inevitably started to look rather drab on them because CSS (hence the browser) didn’t have a way to specify the most saturated colors within those ranges. this was loss of access to as much as around 33% of the range of colors those screens are capable of displaying!

Finally in most browsers, CSS can now support many more colors, offer the ability to specify colors within specialized color spaces (through color() function: see below), and more helpful tools for color manipulation and palette generation than what we had before, such as,

  • perceptual similarity in LCH

you can change a coordinate in any color in the new LCH syntax and the “difference you see” between the first color and the second color will always be the same. in HSL or RGB this is not the case.

  • perceptually linear Lightness coordinate in LCH

two colors with the same Lightness coordinate in LCH will always appear to have the same “lightness.” but in HSL, two colors can have the same Lightness coordinate, but one color can appear wildly darker than the other.

  • less banding in gradients (better color interpolation) and no ‘dead-zones’ or ‘hot-spots.’

how

follows some of the new features introduced by CSS Color Module 4 to specify colors that are no longer restricted to the sRGB gamut.

lch(), oklch(), lab() and oklab() functions for device-independent color

while all four functions serve the same purpose, lch()/oklch() might be better options just because they're more readable and closer to how we realistically think of color - think Lightness-Chroma-Hue in lch()/oklch() vs. Lightness-Green/Redness-Blue/Yellowness in lab()/oklab().

lch(58% 32 241deg / 50%)
/* space-separated arguments (no need for commas anymore)
and optional alpha value separated by a slash (no dedicated functions to specify opacity) */

modeled after human vision, these functions technically can specify those colors and more.

similar to HSL, in LCH (and OKLCH, a better, improved, bug-free version),

  • H represents Hue as an angle (though the values don’t correspond)
  • unlike HSL, Lightness in LCH is predictable (discussed above)
  • and while Saturation is HSL is specified as a percentage between 0 to 100, LCH Chroma is theoretically unbounded and the maximum value also differs from color to color - as it’s designed to be able to represent the entire visible spectrum, not bound to any color-space/device and future proof
  • with an optional opacity value

but now, it’s also very easy to go out of gamut for a display i.e. specify colors that some screens may not be able to show (yet). play with https://oklch.com/ to get an understanding of this.

but in this kind of case, the color will be scaled down (gamut-corrected) to the best match that is displayable in that screen, similar to how it happened back when monitors had narrower gamuts than sRGB.

predefined wide-gamut RGB color spaces

CSS Color 4 has also added the new color() function, which accepts a color space id and a series of R, G and B channel values as parameters, with an optional alpha value.

note how because the parameters are RGB based, argument changes in this function won’t create perceptually uniform colors, and neither is it possible to manipulate colors to create lighter/darker variants, because they are still not human readable. the color() function only makes it possible to specify a wider range of colors.

a little introduction to wider gamuts than sRGB

  • Display P3 - color(display-p3 34% 58% 73%)

with wider adoption in consumer displays, P3 is considered the baseline for HDR displays. supported by Safari since 2017 and all modern Apple displays and many OLED screens.

  • Rec2020 - color(rec2020 34% 58% 73%)

defines the UHDTV standard. not common among handheld consumer devices yet.

  • A98 RGB - color(a98-rgb 34% 58% 73%)

introduced by Adobe to encompass most of the colors achievable on CMYK color printers, by using primary colors (RGB) on a computer display. not common among digital designers.

  • ProPhoto RGB - color(prophoto-rgb 34% 58% 73%)

created by Kodak. around ~12% of the colors are “imaginary,” meaning they are out of visible spectrum.

  • XYZ - color(xyz 22% 26% 53%)
  • XYZ d50 - color(xyz-d50 22% 26% 53%)
  • XYZ d65 - color(xyz-d65 22% 26% 53%)

encompasses all colors that are visible to a person with average eyesight, it is used as a standard reference for other color spaces. the d50 and d60 attributes refer to where white is considered to exist within the color space.

support

at the time of writing, all major browsers except Opera on desktop and Android fully support the color() and lch() (and other) notations.

meanwhile you can check for browser support using a @supports feature query:

@supports (color: color(display-p3 1 1 1)) {}

and check for hardware support with a @media query:

(for instances where the browser may support, say P3 colors, but the display may not)

@media (color-gamut: p3) {} /* CSS */
if (window.matchMedia("(color-gamut: p3)").matches) {} // Javascript

Resources: