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

See auto-generated TOC in header.

Background

This section is non-normative.

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

Historically, CSS has only provided authors with color formats using the RGB model, limited to the sRGB gamut. As CSS is used for more applications (such as print) and displays continue to improve, those limitations become more clear. The CSS Color Level 4 specification defines a number of new color spaces, each with its own syntax, representing both new color models and wider RGB gamuts.

Since all CSS colors up until this point have been restricted to RGB math in the sRGB gamut, Sass has treated all color formats as interchangeable. That has allowed authors to inspect and manipulate colors in any space, without careful management or gamut mapping. It has also allowed Sass to output the most browser-compatible CSS format for any given color.

In order to support the color spaces in CSS Sass will need to start tracking the space and gamut associated with any given color, and provide author tools for managing those color spaces. In addition to supporting the new color space functions, we plan to update all functions in the color module, and provide some additional space and gamut management and inspection functions.

Summary

This section is non-normative.

This proposal defines Sassified versions of all the color functions in CSS Color Level 4. Since the CIE color space defines the entire gamut of visible color, much larger than the target sRGB gamut, out-of-range color definitions will be clipped using a relative-colorimetric approach that leaves in-gamut colors unaffected.

There are several rules of thumb for working with color spaces in Sass:

The oklab() (cubic) and oklch() (cylindrical) functions provide access to an unbounded gamut of colors in perceptually uniform space. Authors can use these functions to define reliably uniform colors. For example, the following colors are perceptually similar in luminosity and saturation:

$pink: oklch(64% 0.196 353); // hsl(329.8 70.29% 58.75%)
$blue: oklch(64% 0.196 253); // hsl(207.4 99.22% 50.69%)

The oklch() format uses consistent ‘lightness’ and ‘chroma’ values, while the hsl() format shows dramatic changes in both ‘lightness’ and ‘saturation’. As such, oklch is often the best space for consistent transforms.

The new color() function provides access to a number of specialty spaces. Most notably, display-p3 is a common space for wide-gamut monitors, making it likely one of the more popular options for authors who simply want access to a wider range of colors. For example, P3 greens are significantly ‘brighter’ and more saturated than the greens available in sRGB:

$fallback-green: rgb(0% 100% 0%);
$brighter-green: color(display-p3 0 1 0);

By default, all Sass color transformations are handled and returned in the color space of the original color parameter. However, all relevant functions now allow specifying an explicit color space for transformations. For example, lightness & darkness adjustments are most reliable in oklch:

$brand: hsl(0 100% 25.1%);

// result: hsl(0 100% 50.1%)
$hsl-lightness: color.adjust($brand, $lightness: 25%);

// result: hsl(6.57 61.7% 57.2%)
$oklch-lightness: color.adjust($brand, $lightness: 25%, $space: oklch);

Note that the returned color is still emitted in the original color space, even when the adjustment is performed in a different space.

Design Decisions

Most of the design decisions involved in the proposal are based on the CSS Color Level 4 specification, which we have tried to emulate as closely as possible, while maintaining support for legacy projects. In some cases, that required major changes to the way Sass handles colors:

  1. RGB channel values are no longer clamped to the gamut of a color space, except for the hsl and hwb spaces, which are unable to represent out-of-gamut colors. By default Sass will output CSS with out-of-gamut colors, because browsers can provide better gamut mapping based on the user device capabilities. However, authors can use the provided color.to-gamut() function to enforce mapping a color into a specific gamut.
  2. RGB channel values are no longer rounded to the nearest integer, since the spec now requires maintaining precision wherever possible. This is especially important in RGB spaces, where color distribution is inconsistent.

We are not attempting to support all of CSS Color Level 5 at this point, since it is not yet implemented in browsers. However, we have used it as a reference while updating color manipulation functions such as color.mix().

Different color spaces often represent different color-gamuts, which can present a new set of problems for authors. Some color manipulations are best handled in a wide-gamut space like oklch, but then need to be mapped back to a narrower gamut like srgb for legacy browsers. We established the following guidelines for color conversion and mapping in Sass color functions:

Unfortunately, the legacy hsl and hwb color spaces are not able to express out-of-gamut colors, even with out-of-range channel values, so any conversion into those spaces (using color.to-gamut() or manipulating colors in those spaces) must always require gamut-mapping into the srgb gamut. This is defined as part of the CSS Color Level 4 specification for converting colors.

Definitions

Color

Note that channel values are stored as specified, maintaining precision where possible, even when the values are out-of-gamut for the known color space.

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 in the rgb, hsl, or hwb color spaces are considered legacy colors. The output of a legacy color is not required to match the input color space, and several color functions maintain legacy behavior when manipulating legacy colors.

This includes colors defined using the CSS color names, hex syntax, rgb(), rgba(), hsl(), hsla(), or hwb() – along with colors that are manually converted into legacy color spaces.

Known Color Space

Sass colors are stored as part of a known color space. Each space has a name and an ordered list of associated channels. Each channel has a name and position index (1-indexed) defined by the space and the order of channels in that space, and a number value with units matching those allowed by the space. Space and channel names match unquoted strings, ignoring case. They are always emitted as unquoted lowercase strings by inspection functions.

Values outside a bounded gamut range are valid, and remain un-clamped, but are considered out of gamut for the given color space. If the channel is bounded, or has a percentage mapping with a lower-boundary of zero, then the channel is considered scalable.

Some color spaces use a polar angle value for the hue channel. Polar-angle hues represent an angle position around a given hue wheel, using a CSS <angle> dimension or number (interpreted as a deg value), and are serialized with deg units.

Colors specified using a CSS color keyword or the hex notation are converted to rgb and serialized as part of the rgb color space.

The known color spaces and their channels are:

Predefined Color Spaces

‘Predefined color spaces’ can be described using the color() function.

The predefined RGB spaces are:

The predefined XYZ spaces 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.

For the sake of interpolating between colors with missing components, the following analogous components are defined by CSS Color Level 4:

| Category | Components | | Reds | r,x | | Greens | g,y | | Blues | b,z | | Lightness | l | | Colorfulness | c,s | | Hue | h |

If any analogous missing components are present, they will be carried forward and re-inserted in the converted color before linear interpolation takes place.

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

A color interpolation method is a space-separated list of unquoted strings, parsed according to the following syntax definition:

ColorInterpolationMethod ::= ‘in’ (
                                  RectangularColorSpace
                                | PolarColorSpace HueInterpolationMethod?
                              )
RectangularColorSpace    ::= ‘srgb’
                           | ‘srgb-linear’
                           | ‘lab’
                           | ‘oklab’
                           | ‘xyz’
                           | ‘xyz-d50’
                           | ‘xyz-d65’
PolarColorSpace          ::= ‘hsl’
                           | ‘hwb’
                           | ‘lch’
                           | ‘oklch’
HueInterpolationMethod   ::= (
                                 ‘shorter’
                               | ‘longer’
                               | ‘increasing’
                               | ‘decreasing’
                               | ‘specified’
                             ) ‘hue’

The resulting interpolation color space is the known color space whose name is given by either the PolarColorSpace or RectangularColorSpace productions.

Different color interpolation methods provide different advantages. For that reason, individual color procedures and functions can establish their own color interpolation defaults, or provide a syntax for authors to explicitly choose the method that best fits their need. The CSS Color Level 4 specification provides additional guidance for determining appropriate defaults.

Procedures

Converting a Color

Colors can be converted from one known 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 known color space target-space, and returns a color output-color.

The algorithms are:

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

Gamut Mapping

Some [known color spaces] describe limited color gamuts. If a color is ‘out of gamut’ for a particular space (most often because of conversion from a larger-gamut color-space), it can be useful to ‘map’ that color to the nearest available ‘in-gamut’ color. Gamut mapping is the process of finding an in-gamut color with the least objectionable change in visual appearance.

Gamut mapping in Sass follows the CSS gamut mapping algorithm. This procedure accepts a color origin in the color space origin color space, and a destination color space destination. It returns the result of a CSS gamut map procedure, which is a color in the destination color space.

This algorithm implements a relative colorimetric intent, and colors inside the destination gamut are unchanged. Since the process is lossy, authors should be encouraged to let the browser handle gamut mapping when possible.

Parsing Color Components

This procedure accepts an input parameter to parse, along with an optional known color space space. It throws common parse errors when necessary, and returns either null or three values: an optional color space, a list of channel numbers, and a floating-point alpha value.

This supports both the space-specific color formats like hsl() and rgb(), where the space is determined by the function, as well as the syntax of color(), where the space is included as one of the input arguments (and may be a user-defined space).

The procedure is:

Percent-Converting a Number

This algorithm takes a SassScript number number and a number max. It returns a number relative to the range [0,max] without clamping.

In order to support both out-of-gamut channels and unbounded ranges, this value is no longer clamped between 0 and max

Normalizing Color Channels

This process accepts an ordered list channels to validate, and a known color space space to normalize against. It throws an error if any channel is invalid for a known color space, or returns a normalized list of valid channels.

Interpolating Colors

This procedure is based on the color interpolation procedures defined in CSS Color Level 4.

This procedure accepts two color arguments (color1 and color2), a [color interpolation method] method, and a percentage weight for color1 in the mix. 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:

Hue Interpolation

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

This process accepts two hue angles (hue1 and hue2), and returns both hues adjusted according to the given method. When no hue interpolation method is specified, the default is shorter.

The process for each hue interpolation method is defined in CSS Color Level 4. If the method is not the value 'specified', both hue angles are set to angle % 360deg prior to interpolation.

Deprecated Functions

Individual color-channel functions defined globally or in the color module are deprecated in favor of the new color.channel() function. That includes:

Legacy global color functions are also deprecated:

While deprecated, if the specified color argument is not a legacy color, throw an error.

New Color Module Functions

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

color.space()

color.to-space()

color.is-legacy()

color.is-powerless()

color.is-in-gamut()

color.to-gamut()

color.channel()

Note that channel values are stored as specified, even if those values are out-of-gamut for the known color space used. Similarly, this color-channel inspection function may return out-of-gamut channel values.

Modified Color Module Functions

color.hwb()

These functions are now deprecated. Authors should use global hwb() instead.

Channel clamping and scaling have been removed from the global function, since we now allow out-of-gamut color-channels to be stored as specified.

color.mix()

mix($color1, $color2,
  $weight: 50%,
  $method: null)

color.change()

change($color, $args...)

This function is also available as a global function named change-color().

color.adjust()

adjust($color, $args...)

This function is also available as a global function named adjust-color().

color.scale()

scale($color, $args...)

This function is also available as a global function named scale-color().

color.complement()

complement($color, $space: null)

This function is also available as a global function named complement().

color.invert()

invert($color, $space: null)

This function is also available as a global function named invert().

color.grayscale()

grayscale($color)

No space argument is provided, since the results should always be in gamut.

This function is also available as a global function named grayscale().

color.ie-hex-str()

This function is also available as a global function named ie-hex-str(). Both functions are deprecated.

ie-hex-str($color)

New Global Functions

These new CSS functions are provided globally.

hwb()

lab()

lch()

oklab()

oklch()

color()

Modified Global Functions

Any legacy global functions that are not explicitly updated here should continue to behave as alias functions for their appropriately updated counterparts.

Note that the new logic preserves decimal values in color channels, as well as preserving the initial color-space used in defining a color.

rgb() and rgba()

The rgba() function is identical to rgb(), except that if it would return a plain CSS function named "rgb" that function is named "rgba" instead.

hsl() and hsla()

The hsla() function is identical to hsl(), except that if it would return a plain CSS function named "hsl" that function is named "hsla" instead.