sass/accepted/color-4-new-spaces.md
2024-08-12 12:16:09 -07:00

92 KiB

CSS Color Level 4, New Color Spaces: Draft 1.20

(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.

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.

  • A color model is a mathematical approach to representing colors and their relationships. Historically, RGB has been the dominant color model for both computer monitors and web browsers. Lately, CIELab and Oklab models have shown significant benefits by providing a more perceptually uniform distribution of colors, so that similar mathematical adjustments achieve visually similar results.

  • A color space is the result of projecting a color model into a coordinate system. In CSS, each color syntax describes a specific (and often unique) color space. For example, rgb(), color(srgb), and color(display-p3) all project the RGB color model into cubic coordinate systems, while hsl() projects the same color model into a cylindrical (polar-angle) space. Similarly, oklab() and oklch() provide different coordinate projections of the Oklab model.

  • A color gamut is the full range of colors that can be described in a color space. Historically, all CSS syntaxes have been limited to the sRGB gamut. However, modern computer monitors often support wider gamuts. Color spaces like srgb and display-p3 describe different gamuts of color, using the same underlying RGB color model, and differently-mapped cubic coordinates.

These terms can get a bit confusing, since there is so much overlap. The term "RGB" can refer to a color model, a color space, a coordinate system, and also a color function. The "RGB" color space is identical to the "sRGB" space, and both describe the "sRGB" gamut. But we have both rgb() and color(srgb) syntax, in order to distinguish legacy from non-legacy variations. They also have different coordinate systems: rgb() accepts a range from 0-255, while color(srgb) accept values from 0-1.

The result is that authors can generally think of each color syntax as a unique space, and each space implies an associated gamut and coordinate system. Color spaces become a simple way to refer to all parts combined. We've used that same approach in Sass, so that the name of a color space can be used to reference the associated gamut. For example, color.to-gamut($color, hsl, $method: local-minde) and color.to-gamut($color, srgb, $method: local-minde) have the same meaning, both mapping a color into the sRGB gamut.

Since all CSS colors up until this point have been restricted to RGB math in the sRGB gamut, Sass has historically treated all color formats and spaces 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/gamut associated with any given color, and provide author tools for managing those color spaces/gamuts. In addition to supporting the new color space functions like color() and lab(), we will update all functions in the color module to support multiple spaces, and provide some additional space and gamut management and inspection functions.

Summary

This section is non-normative.

This proposal defines a Sass representation of colors with color spaces, Sassified versions of all the color functions in CSS Color Level 4, updated definitions of existing Sass functions to accommodate color spaces, as well as several new Sass-specific color functions.

Rules of Thumb

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

  • The rgb, hsl, and hwb spaces are considered "legacy spaces", and will often get special handling for the sake of backwards compatibility. Colors defined using hex notation or CSS color names are considered part of the rgb color space. Legacy colors are emitted in the most compatible format. This matches CSS's own backwards-compatibility behavior.

  • Otherwise, any color defined in a given space will remain in that space, and be emitted in that space.

  • Authors can explicitly convert a color's space by using color.to-space(). This can be useful to enforce non-legacy behavior, by converting into a non-legacy space; or to ensure the color output is compatible with older browsers by converting colors into a legacy space before emitting.

  • The srgb color space is equivalent to rgb, except that one is a legacy space, and the other is not. They also use different coordinate systems, with rgb() accepting a range from 0-255, and srgb using a range of 0-1.

  • Color functions that allow specifying a color space for manipulation will always use the source color space by default. When an explicit space is provided for manipulation, the resulting color will still be returned in the same space as the origin color. For color.mix(), the first color parameter is considered the origin color.

  • All legacy and RGB-style spaces represent bounded gamuts of color. Since mapping colors into gamut is a lossy process, it should generally be left to browsers, which can map colors as-needed, based on the capabilities of a display. For that reason, out-of-gamut channel values are always maintained by Sass, even when converting into gamut-bounded color spaces. Authors can also perform explicit gamut mapping with the color.to-gamut() function.

  • Legacy browsers require colors in the srgb gamut. However, most modern displays support the wider display-p3 gamut.

Standard CSS Color Functions

oklab() 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 353deg); // hsl(329.8 70.29% 58.75%)
$blue: oklch(64% 0.196 253deg); // 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 transformations.

lab() 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()

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

color()

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 to be a popular option 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.

New Sass Color Functions

color.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.is-missing()

This function returns whether a given channel value is "missing" (set to none). This is necessary, since color.channel returns 0 for missing channels. Since color-space conversion can change which channels are missing, this function only supports inspecting channels that are part of the color's own space.

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

// result: false
$missing-lightness: color.is-missing($brand, "lightness");

// result: true
$missing-hue: color.is-missing($brand, "hue");

color.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()

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 color(display-pr 1.2 0 0)). color.is-legacy() returns whether the color is a legacy color in the rgb, hsl, or hwb color space.

color.to-gamut()

This function returns a color that is in the given gamut, using the given mapping algorithm to convert out-of-gamut colors into the desired gamut with as little perceptual change as possible. The current recommended algorithm is local-minde, which is defined in the current candidate recommendation of CSS Color 4, and which will usually be more reliable for generating fallback values than the "channel clipping" approach used by current browsers.

$green: oklch(0.8 2 150);

// oklch(0.91 0.14 164)
$rgb: color.to-gamut($green, "srgb", $method: local-minde);

// oklch(0.91 0.16 163)
$p3: color.to-gamut($green, "display-p3", $method: local-minde);

color.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
$lightness-powerless: color.is-powerless($grey, "lightness");

color.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 unequal.

$orange-rgb: #ff5f00;
$orange-oklch: color.to-space($orange-rgb, oklch);

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

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

Existing Sass Color Functions

color.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()

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

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():

  • color.red()
  • color.green()
  • color.blue()
  • color.hue()
  • color.saturation()
  • color.lightness()
  • color.whiteness()
  • color.blackness()
  • color.alpha()
  • adjust-hue()
  • saturate()
  • desaturate()
  • transparentize()/fade-out()
  • opacify()/fade-in()
  • lighten()/darken()

Design Decisions

Unclamped Channels

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. Channel values are no longer internally clamped to the gamut of a color space. By default Sass will output CSS with out-of-gamut colors, because these colors are handled differently when doing interpolation across different color spaces. Browsers may also eventually handle gamut-mapping for these colors, although at the time of writing they do not. Authors can also use the provided color.to-gamut() function to force a color to be mapped into the gamut of its native color space.

  2. RGB-style 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.

Clamped Channels

Per the CSS specs, certain channels are clamped at parse time but not in interpolation for specific color functions:

  • All channels of the rgb() function.
  • The lower bound of the saturation channel of the hsl() function.
  • The lightness channel of the lab(), lch(), oklab(), and oklch() functions.
  • The lower bound of the chroma channel of the lch() and oklch() functions.

However, it's necessary to use out-of-gamut values in these spaces to represent valid colors from other spaces—for example, color(xyz 1 1 1) is equivalent to lab(100.12% 9.0645 5.8018) and color(prophoto-rgb 0 1 0) is equivalent to rgb(-221.6192400378, 279.4082218845, -109.1140773956). To match the behavior of CSS, we have to clamp these channels when constructing colors directly through color functions, so writing lab(110% 0 0) will return lab(100% 0 0). On the other hand, to preserve colors on a round trip between spaces, we need to allow the internal representation of these colors to go out-of-gamut.

The question remains as to how to handle cases that don't directly correspond to CSS, such as the color.change() function. Because out-of-gamut clamped channels are meaningful in CSS, we've chosen the design principle of preserving them in all situations where clamping isn't specifically mandated by CSS, or in the color.adjust() function to preserve historical behavior.

In addition, to ensure that that the colors represent the same values in CSS that they do in Sass, they'll be serialized to special formats that preserves their out-of-gamut values:

  • Out-of-gamut RGB colors are serialized to hsl(), the lightness channel and saturation upper bound of which per spec are not clamped at parse-time (although in practice browsers do clamp them at time of writing). This ensures that older browsers will still handle these colors somewhat correctly while preserving the unclamped value for modern browsers (once they're standard-compliant).

  • Out-of-gamut Lab, LCH, OKLab, and OKLCH colors are serialized to color-mix(in ..., color(xyz ...) 100%, black) to preserve both the original color space and the unclamped value in conformant browsers.

The Sass specification is intentionally flexible to allow these output formats to change over time in response to evolving specification and browser behaviors.

Conventions for Channels and Space Names

This proposal uses different conventions for referring to the names of color spaces as it does for referring to the names of individual channels within those spaces. Specifically:

  • Color space names are required to be unquoted and matched case-insensitively.

  • Color channels are required to be matched case-sensitively, and to be quoted unless they're used as keyword arguments.

This is motivated by the fact that color space names are a CSS-native construct which are used in the color() and color-mix() functions, and in those contexts are unquoted and case-insensitive; while the channel names, although informed by CSS's color space definitions, are ultimately Sass-specific and follow the more common Sass API conventions. Even when CSS does refer to channels, such as in the relative color syntax of CSS Color 5, it universally refers to them as single characters rather than full names. We wish to avoid these single-character names both for readability and for backwards-compatibility with older Sass APIs.

In addition, requiring that channel names be quoted avoids the issue where unquoted red, green, and blue channels of many colors would be parsed by Sass as color values rather than unquoted strings.

Changing Color Spaces

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 (for now) authors will likely prefer emitting legacy colors that work in existing and legacy browsers. While that is likely to change in the long term, we think it's a worthwhile tradeoff to prioritize author control and legacy color use-cases. Authors who do choose to emit non-legacy colors are less likely to be working in legacy color spaces to begin with. So we've established the following guidelines for color conversion and mapping in Sass color functions:

  • Every color function returns a color in the same space as the original color, no matter what space was used for transformations. The only exception is color.to-space(), which is used for explicit space conversion. Functions that accept two colors (such as color.mix()) return a color in the same space as the first color argument.

  • No color function performs gamut-mapping on out-of-gamut channels except color.to-gamut(), which is used for explicit gamut-mapping.

Gamut Mapping

Browsers currently use channel-clipping rather than the proposed css gamut mapping algorithm to handle colors that cannot be shown correctly on a given display. Moreover, there is still active discussion among the CSS working group and browser vendors about what the best gamut mapping algorithm is, with the currently-specified algorithm widely considered to be sub-optimal. As such, we want to avoid baking it in as the default for Sass such that any change would require a deprecation period followed by a breaking change.

At the same time, we expect gamut-mapping to be very useful for Sass authors working with wide-gamut color spaces, enough so that we want it to be available without needing to wait on the CSSWG to come to a consensus on the best algorithm. To that end, we've decided to provide color.to-gamut() but require a $method argument for forwards-compatibility with better gamut mapping algorithms.

Initially, there will only be two available arguments for $method: local-minde, the gamut mapping algorithm specified in Color 4 at the time this spec is written, and clip which will just clip any channel values that are out-of-range for a given color space. However, we expect to expand this to additional algorithms in the future, and to eventually make it optional and have its default match the behavior of CSS.

CSS Color 5

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().

There is also an open issue in CSS to determine how the relative color syntax from Level 5 should handle missing color components. Since the relative color syntax provides similar functionality to the Sass color.adjust() and color.scale() functions, we have decided to wait and match the CSS behavior once it is specified. In the meantime, Sass will throw errors when trying to adjust or scale a missing component. This is not the ideal behavior, but it provides us with the most flexibility to change our behavior in the future.

Special Thanks

Thanks to the editors of the CSS Color Level 4 specification (Tab Atkins Jr., Chris Lilley, and Lea Verou) for answering our many questions along the way. We also used Chris and Lea's Color.js library as a reference as we developed this proposal.

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:

  • A color space that is a known color space.

  • An ordered list of channels, each one containing a double or the special value none.

  • An alpha that is either the special value none or a double between 0-1 (inclusive).

    While it's valid to specify numbers outside this range, they are meaningless, and can be clamped by input functions when generating a color.

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 spaces will opt users into new default behavior, some legacy color spaces 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.

Legacy colors that have missing components are serialized as non-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.

Color Equality

For determining equality between two colors color1 and color2:

  • If both colors are legacy colors in different spaces, return color.to-space(color1, rgb) == color.to-space(color2, rgb).

  • If the colors are not in the same color space, return false.

  • If either color has a channel (including alpha) marked as missing that the other does not, return false.

  • Return whether each matching pair of non-missing channel values (including alpha) is fuzzy-equal.

    Since this definition no longer involves rounding channels for the legacy RGB space, it is potentially a breaking change. Moving forward, rgb(0 0 0.6) != rgb(0 0 1).

Known Color Space

Each known color space has a name and an ordered list of associated channels. Each channel has a name, and an optional associated unit. Color space names are matched case-insensitively and referred to using unquoted strings, and are always emitted as unquoted lowercase strings by inspection functions. Color channel names are matched case-sensitively and referred to using quoted strings.

Values outside a bounded gamut range (including infinity or negative infinity) are valid but are considered out of gamut for the given color space. They remain un-clamped unless the gamut is specifically marked as "clamped", in which case they're clamped only when constructing the color from its global constructor function or color.adjust().

Some color spaces include a polar angle channel (so far exclusively called hue). Polar-angle hues represent an angle position around a given hue wheel, using a CSS <angle> dimension or number (interpreted as a deg value when no units are passed), and are serialized with deg units. Polar angle channels are normalized using modulo to fall within their conventional range. Any channel that is not polar-angle is considered scalable.

All channels have a conventional range of values, whose bounds are referred to as the channel's lower bound and upper bound (although for unbounded channels these bounds aren't enforced anywhere). For scalable channels, percentages provided by the user are converted to match these bounds. 0% always refers to the value 0, 100% always refers to the upper bound of the range, and -100% always refers to the negative value of the upper bound of the range. (In many cases this is considered outside the conventional scope of the color space, but the a and b channels do conventionally cover negative values as well.)

Colors specified using a CSS color keyword or the hex notation are treated as rgb colors.

The known color spaces and their channels are:

  • rgb (RGB, legacy):

    • red, green, blue:
      • gamut: bounded, clamped

      • range: [0,255]

        Percentages [0%,100%] map to the [0,255] range.

  • hwb (RGB, legacy):

    • hue (polar angle):
      • associated unit: deg
      • range: [0,360]
    • whiteness, blackness:
      • associated unit: %
      • gamut: bounded
      • range: [0,100]
  • hsl (RGB, legacy):

    • hue (polar angle):
      • associated unit: deg
      • range: [0,360]
    • saturation:
      • gamut: bounded, clamped (lower bound only)
      • associated unit: %
      • range: [0,100]
    • lightness:
      • gamut: bounded
      • associated unit: %
      • range: [0,100]
  • srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020 (RGB):

    • red, green, blue:
      • gamut: bounded
      • range: [0,1]
  • xyz, xyz-d50, xyz-d65:

    • x, y, z:
      • gamut: un-bounded
      • range: [0,1]
  • lab:

    • lightness:

      • gamut: un-bounded, clamped
      • associated unit: %
      • range: [0,100]
    • a, b:

      • gamut: un-bounded
      • range: [-125,125]
  • lch:

    • lightness:

      • gamut: un-bounded, clamped
      • associated unit: %
      • range: [0,100]
    • chroma:

      • gamut: un-bounded, clamped (lower bound only)
      • range: [0,150]
    • hue (polar angle):

      • associated unit: deg
      • range: [0,360]
  • oklab:

    • lightness:

      • gamut: un-bounded, clamped
      • associated unit: %
      • range: [0,1]
    • a, b:

      • gamut: un-bounded
      • range: [-0.4,0.4]
  • oklch:

    • lightness:

      • gamut: un-bounded, clamped
      • associated unit: %
      • range: [0,1]
    • chroma:

      • gamut: un-bounded, clamped (lower bound only)
      • range: [0,0.4]
    • hue (polar angle):

      • associated unit: deg
      • range: [0,360]

If a color with negative saturation or chroma would be created in the hsl, lch, or oklch spaces by any means, instead create a color with the absolute value of that saturation or chroma and the hue rotated by 180deg.

This is an equivalent color, and in fact passing channels with negative saturation/chroma through conversion to other color spaces and back will produce this same result. This helps ensure that identical colors are represented uniformly.

Note that this conversion happens after any clipping, so hsl(0deg -50% 50%) returns hsl(0deg 0% 50%), not hsl(180deg 50% 50%).

The xyz-d65 space is an alias for xyz, and implementations should report the space name as xyz even if it was originally written as xyz-d65.

Predefined Color Spaces

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

The predefined RGB spaces are:

  • srgb
  • srgb-linear
  • display-p3
  • a98-rgb
  • prophoto-rgb
  • rec2020

The predefined XYZ spaces are:

  • xyz
  • xyz-d50
  • xyz-d65 (an alias for xyz)

Missing Components

In some cases, a color can have one or more missing components (channel or alpha values). Unless explicitly specified otherwise, missing components are treated as though they had the value 0.

Some situations where missing components are treated specially include interpolation, the == operator, and color space conversion.

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

Category Components
Reds red, x
Greens green, y
Blues blue, z
Lightness lightness
Colorfulness chroma, saturation
Hue hue

Powerless Components

In some color spaces, it is possible for a channel value to be considered powerless in certain circumstances.

  • hsl:

    • If the saturation value is 0%, then the hue channel is powerless.
  • hwb:

    • If whiteness + blackness is greater than or equal to 100%, then the hue channel is powerless.
  • lch/oklch:

    • If the chroma value is 0%, then the hue channel is powerless.

In some circumstances, conversion between color spaces will mark powerless components in the output as missing.

Color Interpolation Method

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

ColorInterpolationMethod ::= RectangularColorSpace
                           | (PolarColorSpace HueInterpolationMethod?)
HueInterpolationMethod   ::= (
                                 'shorter'
                               | 'longer'
                               | 'increasing'
                               | 'decreasing'
                             ) 'hue'

A valid PolarColorSpace is the name of a known color space with a polar angle hue channel. A RectangularColorSpace is the name of any other known color space, without a polar-angle hue. The interpolation color space is the result of looking up a known color space named by either the PolarColorSpace or RectangularColorSpace productions.

ColorInterpolationMethod is case-insensitive.

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.

Serialization

Serialization of Non-Legacy Colors

To serialize a non-legacy color color:

  • If color has a clamped channel whose value is out-of-bounds, emit a CSS <color> expression that evaluates to color's value, then return.

    The specific syntax here is left up to implementations, based on the specifics of the color in question and the realities of browser support. Two options include:

    • color-mix(). For example, color-mix(in lab, color(xyz 1 1 1) 100%, black) will losslessly convert color(xyz 1 1 1) into lab where the native lab syntax would clamp the lightness at 100%.

    • Relative color syntax, which per spec is never clamped. For example, while the lightness in lab(200 50 50) is clamped, the lightness in lab(from black 200 50 50) is not.

    At the time of writing, browser support is patchy for these syntaxes and no browser correctly avoids clipping in all the cases we're relying on. Although we have no way of ensuring that all color values representable in Sass can be correctly loaded by browsers, this spec aims to ensure that Sass generates the correct value according to the CSS spec and that implementations have enough flexibility within that to target the shifting landscape of what browsers actually support.

  • Let components be an empty string.

  • For each channel in color's channels:

    • If channel contains the special value none, append the unquoted string "none" to components.

    • Otherwise:

      • Let channel-number be a number with the same value as channel and unit given by channel's associated unit, if it has one.

      • Append the result of serializing channel-number to components.

    • If channel isn't the last channel, append a space to components.

  • If color's alpha value isn't 1, append the unquoted string " / " followed by the result of serializing a unitless number whose value is color's alpha value to components.

  • Let space-name be color's space name as an unquoted lowercase string.

  • If color's space is not a predefined color space, emit space-name followed by "(", components, and then ")".

    Since a predefined color space is defined as a known color space that uses the color() syntax, this is a reliable way to get the remaining known color spaces that provide their own function syntax.

  • Otherwise, emit "color(", followed by space-name, " ", components, and then ")".

Serialization of Out-of-Gamut RGB Colors

To serialize an out-of-gamut color color in the rgb space:

  • Let hsl be the result of converting color into the hsl space.

  • Return the result of serializing hsl.

Procedures

Looking Up a Known Color Space

This procedure accepts a name, and attempts to look up a known color space with a matching name. It returns the color space named name, or throws an error if name is not the name of a known color space.

  • If name is not an unquoted string, throw an error.

  • If name's value is case-insensitively equal to the name of a known color space, return that color space.

  • Otherwise, throw an error.

    In the future, we can add support for custom/unknown spaces by returning null when no space is found.

Converting a Color

Colors can be converted from one known color space to another. This procedure accepts a color origin-color, and target-space which may be either a known color space or a SassScript value, and returns a color color in target-space.

Since the individual CSS color conversion algorithms don't explicitly handle the process of "carrying over" missing values on analogous channels, we have to handle that here.

  • If target-space is a SassScript value, set it to the result of looking up a known color space named target-space.

  • If origin-color's space is already target-space, return origin-color.

    CSS doesn't perform conversions unless they are required.

  • Let missing be a list of channel names in origin-color that are missing.

  • Let color be the result of css-converting origin-color into target-space.

  • For each channel in missing:

    • If target-space has an analogous component to channel, set the analogous component in color to none.
  • If target-space is not a legacy color space and any channels of color are powerless and not already missing, set those channels to the special value none.

    Don't introduce missing colors to legacy spaces because if users are converting to legacy spaces they're likely targeting older browsers which will choke on none.

  • Return color.

CSS-Converting a Color Space

Algorithms for individual color space conversion are defined in the CSS Color Level 4 specification. CSS color conversion takes a color origin-color, and a known color space target-space, and returns a color output-color.

The individual conversion 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, which is to some degree inherently contextual and subjective.

Gamut mapping takes a color origin, a known color space destination, and an unquoted string method. It returns a color in origin's color space.

  • Let color be the result of converting origin into destination.

  • If method is not case-insensitively equal to one of the methods defined below, throw an error.

  • If color.is-in-gamut(color), return origin.

  • Let mapped be the result of running the method defined below whose name matches method.

  • Return the result of converting mapped into origin's space.

local-minde

The local-minde gamut mapping procedure in Sass follows the 13 February 2024 draft of CSS Color Module Level 4. It returns the result of CSS gamut mapping origin with an origin color space of origin-space and destination of destination.

This algorithm implements a relative colorimetric intent, and colors inside the destination gamut are unchanged.

clip

The clip gamut mapping procedure is not expected to produce good-looking results, but it can be useful to match the current behavior of browsers.

  • Let new-color be a copy of color.

  • For each channel in new-color's channels:

    • If channel is bounded and not missing, set its value to the result of clamping the original value within channel's minimum and maximum values.
  • Return new-color.

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 a single string of components to emit in a CSS function, or three values: a color space, a list of channel values, and an 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.

The procedure is:

  • If input is a special variable string, return an unquoted string with the value of input.

  • If input is a bracketed list, or a list with a separator other than slash or space, throw an error.

  • If input is a slash-separated list:

    • If input doesn't have exactly two elements, throw an error.

    • Otherwise, let components be the first element and alpha the second element of input.

  • Otherwise:

    • Let components be an unbracketed space separated list of all except the last element of input.

    • If the last element of input is an unquoted string that contains /:

      This solves for a legacy handling of / in Sass that would produce an unquoted string when the alpha value is a CSS function such as var(), when either value is none, or when using relative color syntax.

      • Let split-last be the result calling string.split() with the last element of input as the string to split, and / as the separator.

      • If split-last doesn't have exactly two items, return an unquoted string with the value of input.

        This ensures that rgb(1 2 calc(var(--a) / var(--b)) / var(--c)) is handled correctly after the final expresssion is fully converted to a string due to legacy / behavior.

      • If either item in split-last can be parsed as a number, replace the current value of the item with the resulting number value.

      • Let alpha be the second element in split-last, and append the first element of split-last to components.

    • Otherwise, if the last element of input has preserved its status as two slash-separated numbers:

      • Let alpha be the number after the slash, and append the number before the slash to components.
    • Otherwise, append the last element of input to components.

  • If components is an empty list, throw an error.

  • If components is a special variable string:

    • Let channels be the value of components.
  • Otherwise:

    • If components is not an unbracketed space-separated list, throw an error.

    • If the first element of components is an unquoted string which is case-insensitively equal to "from", return an unquoted string with the value of input.

    • If space is null:

      • Let input-space be the first element in components.

      • If input-space is a special variable string, return an unquoted string with the value of input.

      • Set space be the result of looking up a known color space with the name input-space.

      • If space is not a predefined color space, throw an error.

        Only predefined spaces can be passed in as color syntax components. All other known color spaces use explicit functions.

      • Let channels be an unbracketed space-separated list with the remaining elements from components.

    • Otherwise, let channels be the value of components.

    • If any element of channels is not either a number, a special variable string, a special number, or an unquoted string that's case-insensitively equal to "none", throw an error.

  • If alpha is null, let alpha be 1.

  • Otherwise, If alpha is neither a special number nor an unquoted string that's case-insensitively equal to "none":

    • If alpha is not a number, throw an error.

    • Set alpha to the result of percent-converting alpha with a max of 1, and then clamping the value between 0 and 1, inclusive.

  • If channels is a special variable string, or if alpha is a special number, return an unquoted string with the value of input.

  • If any element of channels is a special number:

    • If space is a legacy color space:

      • Let comma-list be the result of calling list.append(channels, alpha, "comma").

      • Return an unquoted string with the value of comma-list.

    • Otherwise, return an unquoted string with the value of input.

      Doing this late in the process allows us to throw any obvious syntax errors, even for colors that can't be fully resolved during compilation.

  • If the length of channels is not 3, throw an error.

    Once special values have been handled, any colors remaining should have exactly the expected number of channels.

  • Set channels to the result of normalizing channels in space.

  • Let space-name be a lowercase unquoted string of space's name.

  • Return space-name, channels channels, and alpha alpha value.

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

  • If number has units other than %, throw an error.

  • Otherwise, if number has the unit %, return number * max / 100%.

  • Otherwise, return number.

Validating a Color Channel

This process accepts a SassScript value value to validate, a known color space space to validate against, and channel a channel in space. It throws an error if the channel is invalid for the color space, or returns a normalized channel value otherwise.

  • If value is not a number or an unquoted string that's case-insensitively equal to "none", throw an error.

  • If value is an unquoted string that's case-insensitively equal to "none", return value.

  • Otherwise:

    • If channel is a polar angle:

      • Let angle be the result of converting value to deg allowing unitless.

      • Return the result of angle % 360deg.

    • Let min and max be the upper and lower bounds of channel's range, respectively.

    • Otherwise, set channel to the result of percent-converting channel with min and max.

    • If this was (transitively) invoked from the global rgb(), lab(), lch(), oklab(), oklch(), or color() functions and valid is a clamped channel, return the result of clamping value within min and max.

    • Return value.

Normalizing Color Channels

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

  • If values is not a list, throw an error.

  • Let normal be an empty list.

  • For each value at position i in values:

    • Let channel be the channel at position i in space's channels.

    • Let valid be the result of validating value as channel in space.

    • Append valid as the next item in normal.

  • Let unitless be an empty list.

  • For each value in normal.

    • If the value of value is the special value none, append none as the next item in unitless.

    • Otherwise, append the value of value as a double without units as the next item in unitless.

  • Return unitless.

Interpolating Legacy Colors

This procedure is based on the legacy behavior of the color.mix() function, but returns a color in the original color1 color space.

This procedure accepts two legacy colors (color1 and color2), and an optional percentage weight for color1 in the mix. It returns a new color mix that represents the appropriate mix of input colors.

  • Let origin-space be color1's color space.

  • Let rgb1 and rgb2 be the result of converting color1 and color2 respectively into rgb.

  • If weight is null, set weight-scale to 0.5.

  • Otherwise, set weight-scale to the result of percent-converting weight with a max of 1, and then clamping the value between 0 and 1 (inclusive).

  • Let normal-weight be weight-scale * 2 - 1.

  • Let alpha1 and alpha2 be the alpha values of rgb1 and rgb2 respectively.

  • Let alpha-distance be alpha1 - alpha2.

  • Let weight-by-distance be normal-weight * alpha-distance.

  • If weight-by-distance == -1, let combined-weight1 be normal-weight.

  • Otherwise:

    • Let weight-distance-sum be normal-weight + alpha-distance.

    • Let combined-weight1 be weight-distance-sum / (1 + weight-by-distance).

  • Let weight1 be (combined-weight1 + 1) / 2.

  • Let weight2 be 1 - weight1.

  • Let red1 and red2 be the red channels of rgb1 and rgb2 respectively.

  • Let red be red1 * weight1 + red2 * weight2.

  • Let green1 and green2 be the green channels of rgb1 and rgb2 respectively.

  • Let green be green1 * weight1 + green2 * weight2.

  • Let blue1 and blue2 be the blue channels of rgb1 and rgb2 respectively.

  • Let blue be blue1 * weight1 + blue2 * weight2.

  • Let alpha be alpha1 * weight-scale + alpha2 * (1 - weight-scale).

  • Let mix be an rgb color with the given red, green, and blue channels, and alpha value.

  • Return the result of converting mix into origin-space.

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 that represents the appropriate mix of input colors.

  • Let origin-space be color1's color space.

  • If weight is null, set weight to 0.5.

  • Otherwise, set weight to the result of percent-converting weight with a max of 1.

  • If weight > 1 or weight < 0, throw an error.

  • If weight == 0, return the result of converting color2 into origin-space.

  • If weight == 1, return color1.

  • Let space be the interpolation color space specified by method.

  • If space is a PolarColorSpace:

    • Let hue-arc be the HueInterpolationMethod specified in method, or shorter if no hue interpolation is specified.
  • Set color1 and color2 respectively to the results of converting color1 and color2 into space.

  • For each color in color1 and color2:

    • If any non-alpha component of color is missing, set that its value to the value of the corresponding component in the other color unless it's missing as well.

    • Set color to the result of premultiplying color.

    • If color's alpha component is missing, set its value to the value of the alpha component in the other color unless it's missing as well.

      This is resolved after premultiplying, because premultiplying has special handling for a missing alpha component.

  • Let mix be a new color in the color space space, with all channel and alpha values initially missing.

  • For each channel of mix:

    • If the corresponding channels of color1 and color2 are missing, continue to the next channel.

    • Let value1 and value2 be the corresponding channel values in color1 and color2 respectively.

    • If channel has a polar angle value, set value1 and value2 respectively to the results of hue interpolation with value1 as hue1, value2 as hue2, using the hue-arc method.

    • Set channel to the result of calculating (value1 * weight) + (value2 * (1 - weight)).

      Channel rounding has been removed, since it is a lossy transform.

  • Set mix the result of un-premultiplying mix.

  • Return the result of converting mix into origin-space.

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.

  • If the color has an alpha value of 1 or none, return color unchanged.

    It's not possible to premultiply channels relative to a missing alpha, and no multiplication is necessary with full opacity.

  • Otherwise, for each channel in color:

    • If the channel is not missing and channel is not a polar angle, set channel's value to the result of multiplying channel's value by color's alpha value.
  • Return the resulting color.

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

  • If color has an alpha value of 1, 0, or none, return color unchanged.

  • Otherwise, for each channel in color:

    • If the channel is not missing and channel is a polar angle, set channel's value to the result of dividing channel's value by color's alpha value.
  • Return the resulting color.

Hue Interpolation

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

This process accepts two angles (hue1 and hue2), and returns both angles adjusted according to an optional hue interpolation method method, which defaults to shorter.

The process for each hue interpolation method is defined in CSS Color Level 4 (matched case-insensitively):

Scaling a Number

This algorithm takes a number number, a value factor, a number max, and an optional number min which defaults to 0. It's written "scale <number> by <factor> with a max of <max> and a min of <min>". It returns a number with the same units as number.

Note that this no longer assumes the original number is in a range of min to max. We now allow scaling up negative numbers, and scaling down numbers above the max value. The inverse operations return the number unchanged, since that's the asymptotic scale behavior approaching boundaries.

  • If factor isn't a number with unit % between -100% and 100% (inclusive), throw an error.

  • If factor > 0%:

    • If number > max, return number.

    • Otherwise, return number + (max - number) * factor / 100%.

  • Otherwise:

    • If number < min, return number.

    • Otherwise, return number + (number - min) * factor / 100%.

New Color Module Functions

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

color.space()

space($color)
  • If $color is not a color, throw an error.

  • Return the name of $colors color space as an unquoted lowercase string.

color.to-space()

to-space($color, $space)
  • If $color is not a color, throw an error.

  • Let converted be the result of converting $color to $space.

  • If converted is a legacy color:

    • For each component in the channels and alpha value of converted, if component is missing, set component to 0.
  • Return converted.

color.is-legacy()

is-legacy($color)
  • If $color is not a color, throw an error.

  • Return true if $color is a legacy color, or false otherwise.

color.is-powerless()

is-powerless($color, $channel, $space: null)
  • If $color is not a color, throw an error.

  • If $channel is not a quoted string, throw an error.

  • Let color be $color if $space is null, and the result of converting $color to $space otherwise.

  • Let channel be the channel in color (case sensitively) named $channel. Throw an error if no such channel exists.

  • Return true if channel is powerless in color, or false otherwise.

color.is-in-gamut()

is-in-gamut($color, $space: null)
  • If $color is not a color, throw an error.

  • Let color be $color if $space is null, and the result of converting $color to $space otherwise.

  • For all bounded channels in color's space, if the associated channel value in $color is fuzzy greater-than the bounded maximum or fuzzy less-than the bounded minimum, return false.

  • Otherwise, return true.

color.to-gamut()

to-gamut($color, $space: null, $method: null)
  • If $color is not a color, throw an error.

  • If $method is not an unquoted string whose value is (case sensitively) either local-minde or clip, throw an error.

  • Otherwise, let destination be the result of looking up a known color space named $space if it's not null, or $color's space otherwise.

  • Return the result of gamut mapping $color with destination destination and method $method.

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.

channel($color, $channel, $space: null)
  • If $color is not a color, throw an error.

  • If $channel is not a quoted string, throw an error.

  • If $channel == "alpha", return $color's alpha value.

  • Let color be $color if $space is null, and the result of converting $color to $space otherwise.

  • Let channel be the channel in color (case sensitively) named $channel. Throw an error if no such channel exists.

  • Let value be channel's value in color, or 0 if the channel's value is missing.

  • If channel's associated unit is %, return value * 100 divided by the maximum of channel's gamut range with unit %.

  • Otherwise, if channel has an associated unit, return value with that unit.

  • Otherwise, return value as a unitless number.

color.is-missing()

is-missing($color, $channel)
  • If $color is not a color, throw an error.

  • If $channel is not a quoted string, throw an error.

  • If $channel == "alpha", return true if $color's alpha value is missing and false otherwise.

  • Let channel be the channel in $color (case sensitively) named $channel. Throw an error if no such channel exists.

  • Return true if channel is missing in $color, and false otherwise.

color.same()

While it's already possible to compare the equality of two colors, the result is always false when the two colors are in different color spaces. This function compares colors across color spaces, to determine if they are equal after being converted into the same space.

same($color1, $color2)
  • Let color1 and color2 be $color1 and $color2, respectively, with any missing components replaced with 0.

    This is necessary to ensure that same() actually matches visual rendering. For example, rgb(100 200 0) and rgb(100 200 none) are rendered identically, but the former converts to color(xyz 0.2590878471 0.4401656621 0.0713080481) while the latter converts to color(xyz 0.2590878471 0.4401656621 none), which are not equivalent if we convert missing channels to zero after XYZ conversion.

  • Let xyz1 and xyz2 be the result of converting color1 and color2 to xyz, respectively.

  • Return xyz1 == xyz2.

Modified Color Module Functions

color.hwb()

This function is now deprecated. Authors should use global hwb() instead.

  • hwb($channels)
    
    • Return the result of calling the global function hwb($channels).
  • hwb($hue, $whiteness, $blackness, $alpha: 1)
    
    • Return the result of calling the global function hwb(list.slash($hue $whiteness $blackness, $alpha)).

color.mix()

mix($color1, $color2,
  $weight: 50%,
  $method: null)
  • If either $color1 or $color2 is not a color, throw an error.

  • If $method is null:

    • If either $color1 or $color2 is not a legacy color, throw an error.

      Method is required for non-legacy colors. This matches the color-mix() function defined in Colors Level 5, and allows us to add additional default behavior in the future.

    • Return the result of legacy interpolation between $color1 and $color2 with the specified $weight.

  • Otherwise, if $method is not a color interpolation method, throw an error.

  • Return the result of interpolating between $color1 and $color2 with the specified $weight and $method.

color.change()

change($color, $args...)

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

  • If $color is not a color, throw an error.

  • If $args contains any positional arguments, throw an error.

  • Let space be the argument named $space in $args, or null if it doesn't have one.

  • Let color be $color if space is null, and the result of converting color to space otherwise.

  • Let alpha be color's alpha property.

  • If the keyword argument $alpha is specified in $args:

    • If alpha isn't a number or an unquoted string that's case-insensitively equal to "none", throw an error.

    • If alpha is a number, set alpha to the result of percent-converting $alpha with a max of 1, and clamping it between 0 and 1 (inclusive).

  • Let channel-args be the remaining keyword arguments in $args, not including $space or $alpha arguments.

  • Let channels be a list of color's channels.

  • For each keyword key and value new in channel-args:

    • If new is not a number or an unquoted string that's case-insensitively equal to "none", throw an error.

      This basic restriction can be applied to all spaces. Further channel restrictions are enforced by the normalization step for known spaces.

    • If key is not the (case-sensitive) name of a channel in channels:

      • If space isn't null, throw an error.

      • If color is not a legacy color, throw an error.

      • If key is one of red, green, or blue, set space to rgb.

      • Otherwise, if key is one of saturation or lightness, or if key is hue and the only other keywords in channel-args are saturation or lightness, set space to hsl.

        This includes the case where hue is the only keyword.

      • Otherwise, if key is one of whiteness or blackness, or if key is hue and the only other keywords in channel-args are whiteness or blackness, set space to hwb.

      • Otherwise, throw an error.

      • Set channels to be a list of the channels in color converted to space.

    • Set the corresponding key value in channels to new.

  • Set channels to the result of normalizing channels in space.

  • Let new-color be a color in space with channels channels and alpha alpha, or a missing alpha if alpha is a string.

  • Return color.to-space(new-color, color.space($color)).

    Call color.to-space() rather than converting $color directly so that this won't cause powerless channels to become unexpectedly missing in legacy color spaces, thereby unexpectedly reducing their browser compatibility.

color.adjust()

adjust($color, $args...)

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

  • If $color is not a color, throw an error.

  • If $args contains any positional arguments, throw an error.

  • Let space be the argument named $space in $args, or null if it doesn't have one.

  • Let color be $color if space is null, and the result of converting $color to space otherwise.

  • Let alpha be the value of color's alpha channel, or the unquoted string none if it's missing.

  • If the keyword argument $alpha is specified in $args:

    • If color's alpha channel is missing, throw an error.

      This is not the ideal solution for handling none, but we want to match CSS relative color syntax if possible. Throwing an error for now means we can adjust to match the CSS behavior once it is defined. See the following issues for details:

    • If $alpha isn't a number, throw an error.

    • Let new-alpha be the result of percent-converting $alpha with a max of 1.

    • Set alpha to the value of new-alpha + alpha clamped between 0 and 1.

  • Let channel-args be the remaining keyword arguments in $args, not including $space or $alpha arguments.

  • Let channels be a list of color's channels.

  • For each keyword key and value adjust in channel-args:

    • If adjust is not a number, throw an error.

    • If key is not the (case-sensitive) name of a channel in channels:

      • If space isn't null, throw an error.

      • If color is not a legacy color, throw an error.

      • If key is one of red, green, or blue, set space to rgb.

      • Otherwise, if key is one of saturation or lightness, or if key is hue and the only other keywords in channel-args are saturation or lightness, set space to hsl.

        This includes the case where hue is the only keyword.

      • Otherwise, if key is one of whiteness or blackness, or if key is hue and the only other keywords in channel-args are whiteness or blackness, set space to hwb.

      • Otherwise, throw an error.

      • Set channels to a list of the channels in color converted to space.

    • Let channel be the channel named key in channels.

    • If channel is missing and space isn't null, throw an error.

      We leave an exemption for null spaces for backwards-compatibility with Sass's behavior before powerless channel support was introduced.

    • Let original be channel's value, or 0 if channel is missing.

      This is not the ideal solution for handling none, but we want to match CSS relative color syntax if possible. Throwing an error for now means we can adjust to match the CSS behavior once it is defined. See the following issues for details:

    • If adjust has the unit %:

      • If channel is not scalable, throw an error.

      • Set adjust to the result of percent-converting adjust with channel's upper bound as max.

      • Otherwise, throw an error.

    • Let value to original + adjust.

      Once percentage/number conversions have been normalized, this will throw an error if adjust and value are not compatible.

    • If channel's upper bound bound is clamped and value > bound:

      • Set value to math.min(original, value) if original > bound and bound otherwise.
    • Otherwise, if channel's lower bound bound is clamped and value < bound:

      • Set value to math.max(original, value) if original < bound and bound otherwise.

    This ensures that adjustment won't ever make a color go out-of-bounds, which preserves the historical clamping behavior (which is particularly important because negative saturation behaves very strangely) while still ensuring that adjustment works rationally for channels that are already out-of-bounds.

  • Set channels to the result of normalizing channels in space.

  • Let new-color be a color in space with channels channels and alpha alpha, or a missing alpha if alpha is a string.

  • Return color.to-space(new-color, color.space($color)).

    Call color.to-space() rather than converting $color directly so that this won't cause powerless channels to become unexpectedly missing in legacy color spaces, thereby unexpectedly reducing their browser compatibility.

color.scale()

scale($color, $args...)

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

  • If $color is not a color, throw an error.

  • If $args contains any positional arguments, throw an error.

  • Let space be the argument named $space in $args, or null if it doesn't have one.

  • Let color be $color if space is null, and the result of converting $color to space otherwise.

  • Let alpha be the value of color's alpha channel, or the unquoted string none if it's missing.

  • If the keyword argument $alpha is specified in $args:

    • If color's alpha channel is missing, throw an error.

      This is not the ideal solution for handling none, but we want to match CSS relative color syntax if possible. Throwing an error for now means we can adjust to match the CSS behavior once it is defined. See the following issues for details:

    • If $alpha isn't a number, throw an error.

    • Set alpha to the result of scaling alpha by $alpha with max 1.

  • Let channel-args be the remaining keyword arguments in $args, not including $space or $alpha arguments.

  • Let channels be a list of color's channels.

  • For each keyword key and value factor in channel-args:

    • If key is not the (case-sensitive) name of a scalable channel in channels:

      • If space isn't null, throw an error.

      • If color is not a legacy color, throw an error.

      • If key is one of red, green, or blue, set space to rgb.

      • Otherwise, if key is one of saturation or lightness, set space to hsl.

      • Otherwise, if key is one of whiteness or blackness, set space to hwb.

      • Otherwise, throw an error.

      • Set channels to a list of the channels in color converted to space.

    • Let channel be the channel named key in channels.

    • If channel is missing in color, throw an error.

      This is not the ideal solution for handling none, but we want to match CSS relative color syntax if possible. Throwing an error for now means we can adjust to match the CSS behavior once it is defined. See the following issues for details:

    • Let min and max be the lower and upper bounds of channel, respectively.

    • Set the corresponding channel in channels to the result of scaling channel by factor with min min and max max.

  • Set channels be the result of normalizing channels in space.

  • Let new-color be a color in space with channels channels and alpha alpha, or a missing alpha if alpha is a string.

  • Return color.to-space(new-color, color.space($color)).

    Call color.to-space() rather than converting $color directly so that this won't cause powerless channels to become unexpectedly missing in legacy color spaces, thereby unexpectedly reducing their browser compatibility.

color.complement()

complement($color, $space: null)

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

  • Return the result of calling color.adjust($color, $hue: 180deg, $space: space).

    This will throw an error if $color is not a color, if space doesn't have a polar-angle hue channel, or if $color's hue is missing (for a non-legacy color or an explicit space).

color.invert()

invert($color,
  $weight: 100%,
  $space: null)

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

  • If invert() was called as a global function, $color is either a number or a special number, $weight == 100%, and $space is null, return an unquoted string representing a CSS function call with name "invert" and argument $color.

  • If $color is not a color, throw an error.

  • If $weight is neither null nor a number with unit %, throw an error.

  • If $space is null:

    • If $color is a legacy color, let color be the result of converting $color to rgb.

    • Let mix-space be null.

      This allows us to also enforce legacy behavior in the final weighted mix.

    • Otherwise, throw an error.

  • Otherwise:

    • Let color be the result of converting $color to $space.

    • Let mix-space be the name of color's space.

  • If $weight == 0%, return $color.

  • If color's space is hwb:

    • Let white and black be the values of color's channels of those names, or the unquoted string "none" if the respective channels are missing.

    • Let invert be the result of calling color.change(color.complement(color, $space: hwb), $white: black, $black: white).

  • Otherwise:

    • Let invert be the value of color.

    • For each channel in color's channels:

      • Let value be channel's value.

      • If channel is a polar-angle hue:

        • If channel is missing, throw an error.

          This is not the ideal solution for handling none, but we want to match CSS relative color syntax if possible. Throwing an error for now means we can adjust to match the CSS behavior once it is defined. See the following issues for details:

        • Let new be (value + 180deg) % 360deg.

      • Otherwise, if channel's name is either chroma or saturation:

        • Let new be channel.
      • Otherwise:

        • If channel is missing, throw an error.

          This is not the ideal solution for handling none, but we want to match CSS relative color syntax if possible. Throwing an error for now means we can adjust to match the CSS behavior once it is defined. See the following issues for details:

        • Let min and max be the lower and upper bounds of channel's range.

        • Let new be max - value if min == 0, and value * -1 otherwise.

      • Set the corresponding channel of invert to new.

  • If $weight == 100%, let result be invert.

  • Otherwise, let result be the result of calling color.mix(invert, color, $weight, mix-space).

  • Return color.to-space(result, color.space($color)).

    Call color.to-space() rather than converting $color directly so that this won't cause powerless channels to become unexpectedly missing in legacy color spaces, thereby unexpectedly reducing their browser compatibility.

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().

  • If grayscale() was called as a global function and $color is either a number or a special number, return an unquoted string representing a CSS function call with name "invert" and argument $color.

  • If $color is a legacy color, return the result of color.change($color, $saturation: 0, $space: hsl).

  • Otherwise, return the result of color.change($color, $chroma: 0, $space: oklch).

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)
  • If $color is not a color, throw an error.

  • Let rgb be the result of converting and gamut mapping $color to rgb with method local-minde.

  • Let alpha be rgb's alpha value.

  • Let hex-alpha be the hexadecimal representation of alpha * 255.

  • Append hex-alpha to hex-list.

  • Let hex-list be an empty list.

  • For each channel in rgba's channels, as numbers:

    • Let hex-channel be the hexadecimal representation of channel's value.

    • Append hex-channel to hex-list.

  • Return the result of concatenating hex-list into a string.

New Global Functions

These new CSS functions are provided globally.

hwb()

  • hwb($channels)
    
    • Let parsed be the result of parsing $channels in hwb space.

      Normalization and clamping is handled as part of the parsing process.

    • If parsed is a string, return a plain CSS function string with the name "hwb" and the argument parsed.

    • Let channels be the channel list, and alpha the alpha value of parsed.

    • Let hue, whiteness, and blackness be the three elements of channels.

    • Return an hwb color with the given hue, whiteness, and blackness channels, and alpha value.

lab()

  • lab($channels)
    
    • Let parsed be the result of parsing $channels in lab space.

    • If parsed is a string, return a plain CSS function string with the name "lab" and the argument parsed.

    • Let channels be the channel list, and alpha the alpha value of parsed.

    • Let lightness, a, and b be the three elements of channels.

    • Return a lab color with the given lightness, a, and b channels, and alpha value.

lch()

  • lch($channels)
    
    • Let parsed be the result of parsing $channels in lch space.

    • If parsed is a string, return a plain CSS function string with the name "lch" and the argument parsed.

    • Let channels be the channel list, and alpha the alpha value of parsed.

    • Let lightness, chroma, and hue be the three elements of channels.

    • Return an lch color with the given lightness, chroma, and hue channels, and alpha value.

oklab()

  • oklab($channels)
    
    • Let parsed be the result of parsing $channels in oklab space.

    • If parsed is a string, return a plain CSS function string with the name "oklab" and the argument parsed.

    • Let channels be the channel list, and alpha the alpha value of parsed.

    • Let lightness, a, and b be the three elements of channels.

    • Return an oklab color with the given lightness, a, and b channels, and alpha value.

oklch()

  • oklch($channels)
    
    • Let parsed be the result of parsing $channels in oklch space.

    • If parsed is a string, return a plain CSS function string with the name "oklch" and the argument parsed.

    • Let channels be the channel list, and alpha the alpha value of parsed.

    • Let lightness, chroma, and hue be the three elements of channels.

    • Return an oklch color with the given lightness, chroma, and hue channels, and alpha value.

color()

  • color($description)
    
    • Let parsed be the result of parsing $description without a space.

    • If parsed is a string, return a plain CSS function string with the name "color" and the argument parsed.

    • Let space be the color space, channels the channel list, and alpha the alpha value of parsed.

    • Return a color in space, with the given channels and alpha value.

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 RGB color channels, as well as preserving the initial color space used when 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.

  • rgb($red, $green, $blue, $alpha: 1)
    
    • If any argument is an unquoted string that's case-insensitively equal to "none", throw an error.

      Missing channels are not allowed in legacy syntax.

    • If any argument is a special number, return a plain CSS function string with the name "rgb" and the arguments $red, $green, $blue, and $alpha.

    • If $alpha is not a number, throw an error.

    • Let alpha be the result of percent-converting alpha with a max of 1, and then clamping the value between 0 and 1, inclusive.

    • Let red, green, and blue be the three elements returned by normalizing ($red, $green, $blue) in rgb space.

    • Return an rgb color with the given red, green, and blue channels, and alpha value.

  • rgb($red, $green, $blue)
    
    • If any argument is a special number, return a plain CSS function string with the name "rgb" and the arguments $red, $green, and $blue.

    • Otherwise, return the result of calling rgb($red, $green, $blue, 1).

  • rgb($channels)
    
    • Let parsed be the result of parsing $channels in rgb space.

    • If parsed is a string, return a plain CSS function string with the name "rgb" and the argument parsed.

    • Let channels be the channel list, and alpha the alpha value of parsed.

    • Let red, green, and blue be the three elements of channels.

    • Return the result of calling rgb(red, green, blue, alpha).

  • rgb($color, $alpha)
    
    • If either argument is a special variable string, return a plain CSS function string with the name "rgb" and the same arguments.

    • If $color is not a legacy color, throw an error.

    • Return the result of calling rgb() with $color's red, green, and blue channels as unitless number arguments, and $alpha as the final argument.

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.

  • hsl($hue, $saturation, $lightness, $alpha: 1)
    
    • If any argument is an unquoted string that's case-insensitively equal to "none", throw an error.

      Missing channels are not allowed in legacy syntax.

    • If any argument is a special number, return a plain CSS function string with the name "hsl" and the arguments $hue, $saturation, $lightness, and $alpha.

    • If $alpha is not a number, throw an error.

    • Let alpha be the result of percent-converting alpha with a max of 1, and then clamping the value between 0 and 1, inclusive.

    • Let hue, saturation, and lightness be the three elements returned by normalizing ($hue, $saturation, $lightness) in the known color space named hsl.

    Conversion to rgb has been removed.

    • Return an hsl color with the given hue, saturation, and lightness channels, and alpha value.
  • hsl($hue, $saturation, $lightness)
    
    • If any argument is a special number, return a plain CSS function string with the name "hsl" and the arguments $hue, $saturation, and $lightness.

    • Otherwise, return the result of calling hsl($hue, $saturation, $lightness, 1).

  • hsl($hue, $saturation)
    
    • If either argument is a special variable string, return a plain CSS function string with the name "hsl" and the same arguments.

    • Otherwise, throw an error.

  • hsl($channels)
    
    • Let parsed be the result of parsing $channels in hsl space.

      Normalization and clamping is handled as part of the parsing process.

    • If parsed is a string, return a plain CSS function string with the name "hsl" and the argument parsed.

    • Let channels be the channel list, and alpha the alpha value of parsed.

    • Let hue, saturation, and lightness be the three elements of channels.

    • Return an hsl color with the given hue, saturation, and lightness channels, and alpha value.

Deprecated Functions

Individual color-channel functions defined globally or in the color module are deprecated in favor of the new color.channel() function. Legacy global color functions are also deprecated. These functions always throw errors. During the deprecation process, they act as alias functions described below.

color.red(), red()

  • color.red($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "red", rgb).

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

color.green(), green()

  • color.green($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "green", rgb).

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

color.blue(), blue()

  • color.blue($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "blue", rgb).

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

color.hue(), hue()

  • color.hue($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "hue", hsl).

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

color.saturation(), saturation()

  • color.saturation($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "saturation", hsl).

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

color.lightness(), lightness()

  • color.lightness($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "lightness", hsl).

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

color.whiteness()

  • color.whiteness($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "whiteness", hwb).

color.blackness()

  • color.blackness($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "blackness", hwb).

color.alpha()

  • color.alpha($color)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.channel($color, "alpha").

adjust-hue()

  • adjust-hue($color, $amount)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.adjust($color, $hue: $amount, $space: hsl).

saturate()

  • saturate($color, $amount)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.adjust($color, $saturation: $amount, $space: hsl).

desaturate()

  • desaturate($color, $amount)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.adjust($color, $saturation: -$amount, $space: hsl).

transparentize(), fade-out()

  • transparentize($color, $amount)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.adjust($color, $alpha: -$amount).

This function is also available as a global function named fade-out().

opacify(), fade-in()

  • opacify($color, $amount)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.adjust($color, $alpha: $amount).

This function is also available as a global function named fade-in().

lighten()

  • lighten($color, $amount)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.adjust($color, $lightness: $amount, $space: hsl).

darken()

  • darken($color, $amount)
    
    • If $color is not a legacy color, throw an error.

    • Return the result of calling color.adjust($color, $lightness: -$amount, $space: hsl).