Sass Color Spaces

Table of Contents

Summary

Historically CSS has been limited to a single color model (RGB) and gamut (sRGB). Authors have various formats to describe colors in that gamut – using cubic hex notation #rgb/#rrggbb/#rrggbbaa and functions rgb()/rgba(), or cylindrical/polar-angle functions hsl()/hwb().

Unfortunately, sRGB is a relatively narrow color gamut, and RGB isn’t a perceptually uniform model. Many monitors now support wider color gamuts (such as the popular display-p3), and there has also been significant progress in developing more perceptually uniform color spaces such as CIE LAB/okLAB.

The CSS Colors level 4 specification describes how CSS authors can access these newer gamuts & formats – with okLAB as the default space for color-mixing. We want to provide support for this in Sass: with server-side tools to manage colors across spaces, and convert colors between spaces where appropriate.

Goals and priorities

(These are initially based on the WCIG color API goals)

Ideally, these tools will be:

Resources

Terms

The differences between a model, space, gamut, and format can become quite blurry:

The current CSS specification uses the term “color space” in reference to both:

There is also a CSS media query feature that uses the term color-gamut with a list of accepted rgb color gamuts.

Sass currently:

The CIE color models (especially XYZ) act as a central standard describing the full range of human-visible color. Other color models are defined relative to this standard, in order to make meaningful translation possible.

Considerations

The CIE-based okLCH format currently provides the best experience for designers in terms of:

Those features are particularly helpful for establishing automated design systems based on color manipulation and relationships.

The downside is that such a wide-gamut format easily allows authors to describe colors that can’t yet be displayed on any modern displays – or colors that will display in some media and not others. When translating colors from a wide gamut into a narrower gamut, any out-of-gamut colors will need to be mapped into the narrower space.

That added complexity also provides the advantage of manipulating colors in a lossless format, and reserving the details of gamut-mapping as an output concern – or even leaving it up to the browser/display.

Gamut mapping in CSS

Colors outside the gamut of a particular space are still considered ‘valid’ as colors, but have to be ‘mapped’ or adjusted to fit the space before they can be displayed.

This should usually be done as late in the process as possible, since it involves data-loss. In most cases, that means we want to leave gamut-mapping for the browser & device to resolve – based on the color space of the display being used. However, there might be some cases where it becomes useful for web authors to handle gamut-mapping manually in their Sass.

In CSS, it is recommended that browsers handle ‘gamut mapping’ with a ‘relative colorimetric intent’ – basically ‘clamping’ colors to the edge of the gamut, while maintaining hue as the primary concern. The spec provides a pseudo-code algorithm for browsers to use when mapping out-of-gamut colors for display.

It seems to me like Sass should provide access to both gamut-checking and gamut-mapping functions – but we may want to plan ahead for potential future improvements, if possible. We will also need to consider carefully when/if it is ever appropriate to automate gamut mapping internally.

Unfortunately, Sass already does automated gamut-mapping of colors into the sRGB space by clamping individual RGB channels – which can cause dramatic hue-shift. Any solution here needs to be backwards-compatible

CSS color spaces

The color() function

The CSS specification allows for loading custom color spaces, but there are also a number of provided spaces – which should be the initial focus for Sass.

There are a number of RGB spaces/gamuts:

And a couple CIE XYZ spaces:

Each of these spaces can be accessed directly using the color() function:

color( <color-space> <coordinates>{3} [ / <alpha> ]? )

(where <coordinates> include percentages for RGB spaces, but only numbers/none in XYZ spaces)

Less used formats (like CMYK) are allowed, but require a linked color profile. Sass should allow this to pass through, but is not likely to support it in any meaningful way.

New format-specific functions

The more heavily recommended formats (CIE lch()/lab() and oklch()/oklab()) and uncalibrated CMYK (device-cmyk()) have their own functions, and are not part of the color() syntax.

CSS color-4 function support

Currently, all of these are supported in Safari (15+):

Relevant Browser issues:

I don’t see open issues for OKlab/OKlch, though both are already supported in Safari.

CSS color-5 function support

There is experimental support for:

A bit farther out, and not yet supported anywhere, all color functions are likely to get a ‘relative syntax’ for quick color adjustments:

.example {
--accent: rgb(from mediumvioletred 255 g b);
color: hsl(from var(--accent) calc(h + 180deg) s l);
}

Prior art

A number of other groups are trying to solve the same issues. Unfortunately, these are all interrelated and updating at different intervals/pace.

Media-query for color-gamut

https://drafts.csswg.org/mediaqueries/#color-gamut

Currently accepts values of srgb | p3 | rec2020.

WICG color API (WIP)

https://github.com/WICG/color-api

colorjs (WIP)

Chris Lilly and Lea Verou (both editors of the latest CSS color specs) have developed colorjs – a color manipulation tool in JS, using many of the same algorithms that are provided in CSS.

That might be useful as a reference for implementation, as well as a reference for use-cases and an author api. That includes:

Currently, colorjs is blurring the distinction between spaces and formats, treating each format as a unique color space. However, in conversations with Chris and Lea, this seems to be an issue they are still struggling with in relation to the proposed web color API.

Possible Sass functionality

Sass currently blurs the lines between different formats in the sRGB gamut, performing interpolation on all of them in cubic RGB space, and generating output in the most concise format available.

So colors do not have a format that authors can access or manipulate, but they could have a space and/or gamut. This seems like the most difficult

CSS color-4 functions

See function details above.

Sass could begin to recognize these new functions as ‘colors’ – with the caveat that we should not default to channel clipping in the sRGB gamut by default.

Only the color() function with srgb-linear space can be safely converted into Sass colors without any additional gamut handling.

For everything else, we would want to extend our understanding of a color to include:

New Sass functions

Some new functions will be needed. For example:

Existing Sass functions

Some existing functions could get additional color-space functionality. For example, both color.mix() and color.complement() could get an optional final parameter for establishing the color space to mix in.

To follow the CSS approach, the default space could be based on the colors provided, such that:

Some existing functions will need to be either updated or duplicated for different formats. For example, color.adjust, color.change, and color.scale all accept channel names – but new formats will cause some of those names (eg hue in hsl vs lch) to have multiple meanings. A similar issue exists with the color-channel functions like color.lightness and color.hue.