Request for Comments: Color Spaces

Posted 21 September 2022 by Miriam Suzanne and Natalie Weizenbaum

There’s been a lot of exciting work in the CSS color specifications lately, and as it begins to land in browsers we’ve been preparing to add support for it in Sass as well. The first and largest part of that is adding support for color spaces to Sass, which represents a huge (but largely backwards-compatible) rethinking of the way colors work.

Historically, all colors in CSS have existed in the same color space, known as sRGB”. Whether you represent them as a hex code, an hsl() function, or a color name, they represented the same set of visible colors you could tell a screen to display. While this is conceptually simple, there are some major downsides:

Color spaces solve all of these problems. Now not every color has a red, green, and blue channel (which can be interpreted as hue, saturation, and lightness). Instead, every color has a specific color space which specifies which channels it has. For example, the color oklch(80% 50% 90deg) has oklch as its color space, 80% lightness, 50% chroma, and 90deg hue.

Color Spaces in Sass permalinkColor Spaces in Sass

Today we’re announcing a proposal for how to handle color spaces in Sass. In addition to expanding Sass’s color values to support color spaces, this proposal defines Sassified versions of all the color functions in CSS Color Level 4.

Rules of Thumb permalinkRules of Thumb

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

Standard CSS Color Functions permalinkStandard CSS Color Functions

oklab() and oklch() permalinkoklab() and oklch()

The oklab() (cubic) and oklch() (cylindrical) functions provide access to an unbounded gamut of colors in a perceptually uniform space. Authors can use these functions to define reliably uniform colors. For example, the following colors are perceptually similar in lightness 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.

lab() and lch() permalinklab() and lch()

The lab() and lch() functions provide access to an unbounded gamut of colors in a space that’s less perpetually-uniform but more widely-adopted than OKLab and OKLCH.

hwb() permalinkhwb()

Sass now supports a top-level hwb() function that uses the same syntax as CSS’s built-in hwb() syntax.

color() permalinkcolor()

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);

Sass will natively support all predefined color spaces declared in the Colors Level 4 specification. It will also support unknown color spaces, although these can’t be converted to and from any other color space.

New Sass Color Functions permalinkNew Sass Color Functions

color.channel() permalinkcolor.channel()

This function returns the value of a single channel in a color. By default, it only supports channels that are available in the color’s own space, but you can pass the $space parameter to return the value of the channel after converting to the given space.

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

// result: 25.1%
$hsl-lightness: color.channel($brand, "lightness");

// result: 37.67%
$oklch-lightness: color.channel($brand, "lightness", $space: oklch);

color.space() permalinkcolor.space()

This function returns the name of the color’s space.

// result: hsl
$hsl-space: color.space(hsl(0 100% 25.1%));

// result: oklch
$oklch-space: color.space(oklch(37.7% 38.75% 29.23deg));

color.is-in-gamut(), color.is-legacy() permalinkcolor.is-in-gamut()color.is-legacy()

These functions return various facts about the color. color.is-in-gamut() returns whether the color is in-gamut for its color space (as opposed to having one or more of its channels out of bounds, like rgb(300 0 0)). color.is-legacy() returns whether the color is a legacy color in the rgb, hsl, or hwb color space.

color.is-powerless() permalinkcolor.is-powerless()

This function returns whether a given channel is “powerless” in the given color. This is a special state that’s defined for individual color spaces, which indicates that a channel’s value won’t affect how a color is displayed.

$grey: hsl(0 0% 60%);

// result: true, because saturation is 0
$hue-powerless: color.is-powerless($grey, "hue");

// result: false
$hue-powerless: color.is-powerless($grey, "lightness");

color.same() permalinkcolor.same()

This function returns whether two colors will be displayed the same way, even if this requires converting between spaces. This is unlike the == operator, which always considers colors in different non-legacy spaces to be inequal.

$orange-rgb: #ff5f00;
$orange-oklch: oklch(68.72% 20.966858279% 41.4189852913deg);

// result: false
$equal: $orange-rgb == $orange-oklch;

// result: true
$same: color.same($orange-rgb, $orange-oklch);

Existing Sass Color Functions permalinkExisting Sass Color Functions

color.scale(), color.adjust(), and color.change() permalinkcolor.scale(), color.adjust(), and color.change()

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% 43.8%)
$hsl-lightness: color.scale($brand, $lightness: 25%);

// result: hsl(5.76 56% 45.4%)
$oklch-lightness: color.scale($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.

color.mix() permalinkcolor.mix()

The color.mix() function will retain its existing behavior for legacy color spaces, but for new color spaces it will match CSS’s “color interpolation” specification. This is how CSS computes which color to use in between two colors in a gradient or an animation.

Deprecations permalinkDeprecations

A number of existing functions only make sense for legacy colors, and so are being deprecated in favor of color-space-friendly functions like color.channel() and color.adjust():

Let Us Know What You Think! permalinkLet Us Know What You Think!

There’s lots more detail to this proposal, and it’s not set in stone yet. We want your feedback on it! Read it over on GitHub, and file an issue with any thoughts or concerns you may have.