Gamut mapping
CSS currently provides several ways to define and manipulate colors, that are independent of any particular RGB gamut, such as oklch() or lab().
While this makes it easier to define and manipulate colors in ways that are closer to human perception, it also makes it much easier to define colors that fall outside of the display gamut of any output device, especially when modifying existing colors (that are in gamut to begin with!), such as when creating color ramps from a dynamic accent color.
For example, pick any non-neutral accent color (e.g. #0075ff, Chrome’s default AccentColor value), and adjust its OKLCh lightness to 97% to produce a tinted background color. The result (oklch(from #0075ff 97% c h) or oklch(0.97 0.2235 258.41)) is outside the display gamut of any conceivable display device. Without gamut mapping (i.e. currently in most UAs), this is displayed via converting to RGB, and clipping red, green, blue coordinates. This produces colors dramatically different than the specified color, losing all guarantees that these color spaces were supposed to provide.
This carries accessibility implications as well, since RGB clipping can significantly affect contrast characteristics, making color pairs inaccessible. See here a particularly egregious example where white is displayed as blue in non-GM browsers (bsky post).
To display these colors correctly they need to be mapped to the closest in-gamut color, typically by reducing chroma (≈ saturation) while preserving hue and lightness. Here is a visual of the example above, with each color as a dot shown relative to the P3 gamut:
#0075ff |
oklch(from #0075ff 97% c h) |
oklch(from #0075ff 97% c h), gamut mapped to P3 |
|
|---|---|---|---|
| Color | <img width="442" height="209" alt="Image" src="https://github.com/user-attachments/assets/594680e4-150b-4591-a582-ae406552d8c3" /> | <img width="442" height="209" alt="Image" src="https://github.com/user-attachments/assets/4d1804fa-bddf-45da-85e1-a3fc6c4119f9" /> | <img width="442" height="209" alt="Image" src="https://github.com/user-attachments/assets/dab1f040-ddfe-41f3-86cd-4342b948b66f" /> |
| Color relative to P3 gamut | <img width="472" height="456" alt="Image" src="https://github.com/user-attachments/assets/4c00090a-e2b1-4b29-bd26-484be3307244" /> | <img width="472" height="447" alt="Image" src="https://github.com/user-attachments/assets/d730345f-db2a-4537-864e-2c2779d00ba9" /> | <img width="468" height="452" alt="Image" src="https://github.com/user-attachments/assets/e5afd9a0-fc36-4a99-a69d-4af3ede7bf35" /> |
Notice how the gamut shrinks dramatically as we get towards white (100%) or black (0%), so a color that was perfectly in-gamut for midtones, can be wildly out of gamut for lighter tints. We promised authors that color spaces like oklch() + relative colors would let them create dynamic color ramps, but that promise was never realized due to the lack of GM.
CSS Color 4 defines three gamut mapping algorithms that browsers can pick from (comparison), but so far only Firefox has shipped an implementation. Soon, it may be too late to fix this due to web compat.
This was previously submitted in 2024 by @romainmenke in #443 and rejected on the basis of lacking testing infrastructure. There are currently more tests than there were back then. Canvas makes it easier to test at least for specific colorSpace values.
In terms of implementations, Firefox has implemented GM behind a flag and Chrome and WebKit are reportedly looking into it.
Discussion on GitHub (2)
Related issues:
Please don't comment inside the linked issues about how you want gamut mapping, we all want it.
The linked issues are for figuring out the right way to achieve it.
@LeaVerou please add these two tests to your original post: