Sass Color Spaces Proposal

Table of Contents

(Issue)

This proposal adds Sass support for several new CSS color spaces defined in CSS Color Level 4 – including access to non-RGB color models and colors outside the sRGB gamut.

Table of Contents

Background

This section is non-normative.

When working with color on the web, there are a few important terms:

New CSS Color Spaces

The CSS Color Level 4 specification defines a number of new color spaces, each with its own syntax, but representing both new color models and wider color gamuts.

CSS color manipulation & interpolation functions will use the OKLab color space by default, unless otherwise defined for specific functions, or unless legacy behavior needs to be maintained. The CIE model will act a central reference for moving between spaces. Both models can be accessed in either cubic (LAB) or cylindrical (LCH) space:

The new color() function provides access to a number of less common spaces. The CIE XYZ spaces act as a central reference for conversion, with a gamut that covers all human-visible colors.

The remaining spaces are all extensions of the RGB color model, providing wider gamuts, improved bit-depth precision, or removed gamma-encoding:

Since the display-p3 color space represents a common space for wide-gamut monitors, that is likely to be one of the more popular color spaces for authors who simply want access to a wider range of colors – and don’t require the improved uniformity of manipulation or ease-of-use provided by CIE & OK spaces. The sRGB syntax is also useful as a way to ‘opt out’ of legacy color handling for RGB colors.

Missing & Powerless Color Components

CSS Color Level 4 also adds support for ‘missing’ & ‘powerless’ color components. For example, when converting a grayscale color to a polar-angle space like hsl, the resulting hue is unknown. Similarly, at the extremes of lightness (eg black or white), both hue and saturation become ‘powerless’ – changing them has no impact.

This can now be represented in CSS & Sass by using a value of none, so that the color white becomes effectively hsl(none none 100%). The none value is treated as 0 in most cases, but when interpolating two colors, a none value takes it’s value from the other color involved.

This also allows interpolating specific channels of a color. For example, any color can be moved towards ‘grayscale’ by mixing it with the color oklch(none 0% none).

Color Conversion and Gamut Mapping

The CSS Color Level 4 specification defines algorithms for conversion between all the new and existing color spaces. Still, since some spaces provide access to wider gamuts than others, it is possible for a color defined or manipulated in one color space to be ‘out-of-gamut’ when converted to another space.

There are various possible ways to ‘map’ an out-of-gamut color to its nearest in-gamut relative, and the spec provides some advice on how that can be done. In order to avoid data-loss, it’s often best to leave gamut-mapping as a detail for the browser to manage, based on the gamut of the display. Still, it would be good for Sass to provide some explicit gamut-mapping tools for authors to use when converting newer color-systems into backwards-compatible output.

The primary approach is to reduce the chroma value of a color in OKlch space, until it is reasonably ‘close’, and then clamping channel values in the destination color space. Chroma reduction helps avoid more noticeable shifts in lightness and hue, while the final clamping helps avoid dramatic chroma shifts when a more subtle movement is possible.

Browser Support

WebKit/Safari is already shipping support for all these new color spaces, and Gecko/Firefox has released some early drafts behind an experimental flag. These features are also included as a goal of [Interop 2022][interop], which makes them likely to roll out in all browsers by the end of the year.

Summary

This section is non-normative.

This proposal defines global (un-prefixed) Sass support for all of the color functions in CSS Color Level 4 (hwb(), lab()/lch(), oklab()/oklch(), color()). All (new and existing) color functions are also extended to support both:

Additionally, this proposal provides new functions in the sass color module for inspecting a color’s space, as well as converting & gamut-mapping across different spaces.

Design Decisions

Because all previously-available CSS color spaces provided the same gamut of colors using the same color model, both CSS & Sass have considered them interchangeable, converting between them silently, and using RGB/a internally. In order to move forward while maintaining backwards compatibility, both CSS & Sass will need to maintain some legacy handling for these legacy colors, while providing a way to opt into the new defaults used by newer color syntax.

The proposed solution in CSS is that:

CSS serialization converts all hex, rgb(a), hsl(a), hwb, and named colors to their rgb(a) equivalent, as part of a shared srgb space. However:

We have attempted to match this behavior.

Definitions

Color

A color is an object with several parts:

Legacy Color

Both Sass and CSS have similar legacy behavior that relies on all colors being interchangeable as part of a shared srgb color space. While the new color formats will opt users into new default behavior, some legacy color formats behave differently for the sake of backwards-compatibility.

Colors that are defined using the CSS color names, hex syntax, rgb(), rgba(), hsl(), hsla(), or hwb() – along with colors that result from legacy interpolation – are considered legacy colors. All legacy colors use the srgb color space, with red, green, and blue channels. The output of a legacy color is not required to match the input syntax.

Color Space

Every color is stored internally as part of a defined color space. Each space has a name, and an ordered list of associated channels that can be accessed and manipulated in that space. Each channel value can be any number, or the keyword none.

Bounded channels have a clearly-defined range that can be mapped to percentage values and scaled, even if those channels are not clamped to the range given.

This follows the CSS specification, which defines percentage-mapping onto several channels that are technically unbounded. However, some channels (marked below) are percentage-mapped without a clear boundary for scaling.

Legacy colors are converted to srgb internally, with any missing channels (specified using the none keyword) set to 0.

The color spaces and their channels are:

Missing Components

In some cases, a color can have one or more missing components (channel or alpha values). Missing components are represented by the keyword none. When interpolating between colors, the missing component is replaced by the value of that same component in the other color. In all other cases, the missing value is treated as 0.

Powerless Components

In some color spaces, it is possible for a channel value to become ‘powerless’ in certain circumstances. If a powerless channel value is produced as the result of color-space conversion, then that value is considered to be missing, and is replaced by the keyword none.

Color Interpolation Method

<x><pre>
**ColorInterpolationMethod** ::= 'in' (
&#32;                                 RectangularColorSpace
&#32;                               | PolarColorSpace HueInterpolationMethod?
&#32;                             )
**RectangularColorSpace**    ::= 'srgb'
&#32;                          | 'srgb-linear'
&#32;                          | 'lab'
&#32;                          | 'oklab'
&#32;                          | 'xyz'
&#32;                          | 'xyz-d50'
&#32;                          | 'xyz-d65'
**PolarColorSpace**          ::= 'hsl'
&#32;                          | 'hwb'
&#32;                          | 'lch'
&#32;                          | 'oklch'
**HueInterpolationMethod**   ::= (
&#32;                                'shorter'
&#32;                              | 'longer'
&#32;                              | 'increasing'
&#32;                              | 'decreasing'
&#32;                              | 'specified'
&#32;                            ) 'hue'
</pre></x>

Different color interpolation methods provide different advantages. For that reason, individual color procedures and functions (the host syntax) can establish their own color interpolation defaults, or provide a syntax for authors to explicitly choose the method that best fits their need.

The host syntax for a given interpolation procedure is the color syntax or function that instigates that interpolation. When selecting a color interpolation method:

Hue Interpolation Methods

When interpolating between polar-angle hue channels, there are multiple ‘directions’ the interpolation could move, following different logical rules.

The hue interpolation methods below are defined in CSS Color Level 4. Unless the type of hue interpolation is the value specified, both angles need to be constrained to [0, 360) prior to interpolation.

One way to do this is n = ((n % 360) + 360) % 360.

When no hue interpolation method is given, the default is shorter.

Procedures

Converting a Color

Colors can be converted from one color space to another. Algorithms for color conversion are defined in the CSS Color Level 4 specification. Each algorithm takes a color origin-color, and a string target-space, and returns a color output-color.

Colors defined in an RGB color space using the the color() function syntax, are referred to as predefined RGB spaces.

The algorithms are:

For additional details, see the Sample code for color conversions.

Parsing Color Components

This procedure accepts an input argument input to parse, along with a max argument representing the number of color channels allowed, then throws any common errors if necessary, and returns either null (if the syntax contains special CSS values), or a list of parsed values:

Interpolating Colors

This procedure accepts two color arguments (color-1 and color-2), an optional color interpolation method method, and a percentage distance along the resulting interpolation path. It returns a new color mix that represents the appropriate mix of input colors.

Premultiply Transparent Colors

When the colors being interpolated are not fully opaque, they are transformed into premultiplied color values. This process accepts a single color and updates the channel values if necessary, returning a new color with premultiplied channels.

The same process can be run in reverse, to un-premultiply the channels of a given color:

Color Module Functions

These new and modified functions are part of the sass:color built-in module.

space()

to-space()

is-legacy()

is-powerless()

is-in-gamut()

to-gamut()

channel()

Global Functions

These new CSS functions are provided globally.

hwb()

lab()

lch()

oklab()

oklch()

color()