Skip to content

Latest commit

 

History

History
1726 lines (1123 loc) · 56.6 KB

proposal.md

File metadata and controls

1726 lines (1123 loc) · 56.6 KB

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

  • 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 format describes a specific (and often unique) color space. For example, rgb() projects the RGB color model into a cubic coordinate system, while hsl() projects the same model into a cylindrical (polar-angle) space. Different spaces will have different benefits when adjusting or interpolating colors for different purposes.
  • A color gamut is the full range of colors that can be described in a color space. Historically, all CSS spaces have been limited to the same sRGB gamut. However, modern computer monitors often support wider gamuts like display-p3.

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 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.
  • 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.
  • 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 spaces represent bounded gamuts of color. When converting ( colors into a bounded gamut space, out-of-gamut channel values are maintained whenever possible. The only exception is that hsl and hwb color spaces are not able to express out-of-gamut color, so converting colors into those spaces will require gamut-mapping.
  • Mapping colors into a gamut is a lossy process. Whenever possible, it should be left to the browser, which can map colors based on a given user's display capabilities. However, authors can 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.

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:

  • 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 can be used for manual space conversion. Functions that accept two colors (e.g. 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 can be used for manual gamut-mapping.

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:

  • A color space that is either a known color space or an unquoted string.

  • An ordered list of numeric channel values.

  • A floating-point number alpha value 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 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:

  • rgb (RGB, legacy):

    • red, green, blue:
      • gamut: bounded

      • number: [0,255]

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

  • hwb (RGB, legacy):

    • hue: polar angle
    • whiteness, blackness:
      • gamut: bounded
      • percentage: [0%,100%]
  • hsl (RGB, legacy):

    • hue: polar angle
    • saturation, lightness:
      • gamut: bounded
      • percentage: [0%,100%]
  • srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020 (RGB):

    • red, green, blue:
      • gamut: bounded

      • number: [0,1]

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

  • xyz, xyz-d50, xyz-d65:

    • x, y, z:
      • gamut: un-bounded, scalable

      • number: [0,1]

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

  • lab:

    • lightness:

      • gamut: un-bounded, scalable

      • number: [0,100]

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

    • a, b:

      • gamut: un-bounded

      • number: [-125,125]

        Percentages [-100%,100%] map to the [-125,125] range.

  • lch:

    • lightness:

      • gamut: un-bounded, scalable

      • number: [0,100]

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

    • chroma:

      • gamut: un-bounded, scalable

      • number: [0,150]

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

    • hue: polar angle

  • oklab:

    • lightness:

      • gamut: un-bounded, scalable

      • number: [0,1]

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

    • a, b:

      • gamut: un-bounded

      • number: [-0.4,0.4]

        Percentages [-100%,100%] map to the [-0.4,0.4] range.

  • oklch:

    • lightness:

      • gamut: un-bounded, scalable

      • number: [0,1]

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

    • chroma:

      • gamut: un-bounded, scalable

      • number: [0,0.4]

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

    • hue: polar angle

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

  • hsl:

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

    • If the lightness value is either 0% or 100%, then both the hue and saturation values are powerless.

  • hwb:

    • If the combined whiteness and blackness values (after normalization) are equal to 100%, then the hue channel is powerless.
  • lab/oklab:

    • If the lightness value is 0%, then both the a and b channels are powerless.

    The current spec has an inline issue asking if high values of lightness (whites) should make the a and b values powerless: See: https://drafts.csswg.org/css-color-4/#issue-e05ac5c3

  • lch/oklch:

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

    • If the lightness value is 0%, then both the hue and chroma channels are powerless.

    The current spec has an inline issue asking if high values of lightness (whites) should make the hue and chroma values powerless. See: https://drafts.csswg.org/css-color-4/#issue-1813c844

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:

  • If input is a special variable string, return null.

  • Let include-space be true if space is null, and false otherwise.

  • 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 /:

      • 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 there are not two items in split-last, throw an error.

      • If either item in split-last can be coerced to 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.

      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() or when either value is the keyword none.

    • 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 space is null:

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

      • If input-space is not either a known color space or an unquoted string, throw an error.

      • Let space be the value of input-space.

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

    • Otherwise, let channels be the value of components.

    • Let expected be the number of channels in space if space is a known color space, and null otherwise.

    • If any element of channels is not either a number, a special variable string, a special number string, or the keyword none, throw an error.

  • If alpha is null, let alpha be 1.

  • Otherwise, If alpha is not a special number string:

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

    • Otherwise, throw an error.

  • If space or channels is a special variable string, or if alpha is a special number string, return null.

  • If any element of channels is a special number string, return null.

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

  • If expected is not null, and the length of channels is not equal to expected, throw an error.

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

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

  • If include-space is true, let parsed be an unbracketed space-separated list with space as the first element, and normal as the second.

  • Otherwise, let parsed be the value of normal.

  • Return an unbracketed slash-separated list with parsed as the first element, and alpha as the second.

    This results in valid CSS color-value output, while also grouping space, channels, and alpha as separate elements in nested lists. Alternately, we could allow parsed to be a single flat list, even when the color-space is included?

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.

  • If number has the unit %, set number to number * max / 100, without units.

  • Return number.

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.

  • If space is not a known color space or an unquoted string, throw an error.

  • If channels is not an ordered list, throw an error.

  • Let normal be an empty list.

  • For each channel in channels:

    • If channel is not a number or the keyword none, throw an error.

    • If channel is the keyword none, or if space is not a known color space, append channel as the next item in normal.

      We don't attempt further channel normalization for unknown color spaces.

    • Otherwise:

      • Let valid be the corresponding channel defined by the known color space space.

      • If valid is a polar-angle hue:

        • Let normal-channel be the result of converting channel to deg allowing unitless.

        • Append normal-channel as the next item in normal.

        Normalizing the result into a half-open range of [0,360) would be a lossy transformation, since some forms of hue interpolation require the specified hue values.

      • Otherwise, if valid requires a percentage:

        • If channel is a number with units other than %, throw an error.

        • Append channel as the next item in normal.

      • Otherwise:

        • Set channel to the result of percent-converting channel with a max defined by the valid channel range.

        • Append channel as the next item in normal.

  • Return normal.

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.

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

  • If weight is null, set weight to 50%.

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

  • Otherwise:

    • If method is not a color interpolation method, throw an error.

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

    • If space is a PolarColorSpace:

      • Let hue-arc be the HueInterpolationMethod specified in method, or shorter if no hue interpolation is specified.
  • For each color of color1 and color2:

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

    • If origin-space is not a known color space, throw an error.

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

    • Set color to the results of converting color into space.

    • For each channel in missing:

    • If any component of color is none, set that component to the value of the corresponding component in the other color.

      If both values are none, the interpolation result for that component will also be none.

    • Set color to the result of premultiplying color.

  • Let mix be a new color in the known color space space, with none for alpha and all channel values.

  • For each channel of mix:

    • Let channel1 and channel2 be the corresponding channel values in color1 and color2 respectively.

    • If channel represents a hue angle, set channel1 and channel2 respectively to the results of hue interpolation with channel1 as hue1, channel2 as hue2, using the hue-arc method.

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

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

  • Return the result of un-premultiplying mix.

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 value is none, or if channel represents a polar-angle hue, keep the original value of channel.

    • Otherwise, set channel to the result of multiplying the channel value by the alpha value.

  • Return the resulting color with premultiplied channels.

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 value none, or if channel represents a polar-angle hue, keep the original value of channel.

    • Otherwise, set channel to the result of dividing the premultiplied channel value by the alpha value.

  • Return the resulting color with un-premultiplied channels.

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:

  • color.red()/red()
  • color.green()/green()
  • color.blue()/blue()
  • color.hue()/hue()
  • color.saturation()/saturation()
  • color.lightness()/lightness()
  • color.whiteness()
  • color.blackness()

Legacy global color functions are also deprecated:

  • adjust-hue()
  • saturate()/desaturate()
  • transparentize()/opacify()/fade-in()
  • lighten()/darken()
  • adjust-color()/change-color()/scale-color()
  • mix()/complement()/invert()/grayscale()

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

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

    • Return an unquoted string with the name of $colors known color space.

color.to-space()

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

    • Let origin-space be the result of calling color.space($color).

    • If origin-space == $space, return $color.

      This allows unknown spaces, as long as they match the origin space.

    • If either origin-space or $space is not a known color space, throw an error.

    • Return the result of converting the origin-color $color to the target-space $space.

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 $space is null:

      • Let color be $color, and let space be the result of calling space($color).
    • Otherwise:

      • Let color be the result of calling color.to-space($color, $space), and let space be $space.
    • If space is not a known color space, throw an error.

    • If $channel is not the name of a channel in the color-space space, throw an error.

    • Return true if the channel $channel is powerless in color, otherwise return false.

color.is-in-gamut()

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

    • Let space be the value of $space if specified, or the result of calling color.space($color) otherwise.

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

    • Let color be the result of calling color.to-space($color, space).

    • For all bounded channels in space, if the associated channel value in $color is outside the bounded range, return false.

    • Otherwise, return true.

color.to-gamut()

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

    • Let origin-space be the result of calling color.space($color).

    • Let target-space be the value of $space if specified, or the value of origin-space otherwise.

    • If target-space is not a known color space, throw an error.

    • Return the result of gamut mapping with $color as the origin color, origin-space as the origin color space, and target-space as the destination color space.

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 $space is null:

      • Let space be the result of calling color.space($color), and let color be the value of $color.
    • Otherwise:

      • Let color be the result of calling color.to-space($color, $space), and let space be the value of $space.
    • Let channels be a map 1-indexed integer channel keys and their corresponding values in color.

    • If space is a known color space:

      • Let named-channels be a map of channel names defined by space, and their corresponding values in color.

      • Set channels to the result of map.merge(channels, named-channels).

    • Let value be the result of calling map.get(channels, $channel).

    • If value is null, throw an error.

    • Otherwise, return value.

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.

  • 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($hue $whiteness $blackness / $alpha).

color.mix()

mix($color1, $color2,
  $weight: 50%,
  $method: null)
  • If either $color1 or $color2 is not a color with a known color space, 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.

    • Let color1 and color2 be the result of converting $color1 and $color2 respectively into the rgb known color space.

    • Let weight-scale be the result of percent-converting $weight with a max of 1, and 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 color1 and color2, 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 color1 and color2 respectively.

    • Let red be the result of rounding red1 * weight1 + red2 * weight2 to the nearest integer.

    • Let green1 and green2 be the green channels of color1 and color2 respectively.

    • Let green be the result of rounding green1 * weight1 + green2 * weight2 to the nearest integer.

    • Let blue1 and blue2 be the blue channels of color1 and color2 respectively.

    • Let blue be the result of rounding blue1 * weight1 + blue2 * weight2 to the nearest integer.

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

    • Return a legacy color in the rgb space, with the given red, green, and blue channels, and alpha value.

  • Otherwise:

  • If space is unquoted and space == hsl or space == hwb:

    These color spaces are unable to express colors outside the srgb gamut.

  • Otherwise, let color1 and color2 be the result of converting $color1 and $color2 respectively into space.

  • 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 any item in $args is not a keyword argument, throw an error.

  • Let space be $color's color space.

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

    • If $space is not a known color space, and $space != space, throw an error.

    • Set space to the value of $space.

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

  • Otherwise, let color be the value of $color.

  • Let legacy be true if $color is a legacy color and space is a legacy color space, and false otherwise.

  • Let alpha be color's alpha property.

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

    • Set alpha to the result of percent-converting $alpha, 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 the color's channels.

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

    • If key is a string in the format channel<integer>, set key to the value of <integer>.

      This allows e.g. color.change($color, $channel1: 0.25) for changing color channels in unknown color spaces.

    • If key is not the name or index of a channel in channels, throw an error.

    • Set the corresponding channel in channels to new.

  • If space is a known color space, set channels to the result of normalizing channels in space.

  • Return a color in color space space, with channels channels, and an alpha of alpha.

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 any item in $args is not a keyword argument, throw an error.

  • Let space be $color's color space.

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

    • If $space is not a known color space, and $space != space, throw an error.

    • Set space to the value of $space.

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

  • Otherwise, let color be the value of $color.

  • Let legacy be true if $color is a legacy color and space is a legacy color space, and false otherwise.

  • Let alpha be color's alpha property.

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

    • If $alpha is not a unitless number between -1 and 1 (inclusive), throw an error.

    • Set alpha to the value of $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 the color's channels.

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

    • If key is a string in the format channel<integer>, set key to the value of <integer>.

      This allows e.g. color.change($color, $channel1: 0.25) for changing color channels in unknown color spaces.

    • If key is not the name or index of a channel in channels, throw an error.

    • Set the corresponding channel in channels to channel + adjust, treating any none keyword as a value of 0.

  • If space is a known color space, set channels to the result of normalizing channels in space.

  • Return a color in color space space, with channels channels, and an alpha of alpha.

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 any item in $args is not a keyword argument, throw an error.

  • Let space be $color's known color space.

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

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

    • Set space to the value of $space.

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

  • Otherwise, let color be the value of $color.

  • Let legacy be true if $color is a legacy color and space is a legacy color space, and false otherwise.

  • Let alpha be color's alpha property.

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

    • If $alpha is not a unitless number between -1 and 1 (inclusive), throw an error.

    • Set alpha to the result of [scaling] alpha by $alpha with max 1, treating any none keyword as a value of 0.

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

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

  • For each keyword scale in channel-args:

    • If scale is not the name of a bounded or percentage-mapped channel in channels, throw an error.

    • Set the corresponding channel in channels to the result of [scaling] channel by scale with a max defined by the channel boundary.

  • Let normal be the result of normalizing channels.

  • Return a color in color space space, with normal channels, and an alpha of alpha.

color.complement()

complement($color, $space: null)

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

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

  • If $space is null:

    • If $color is a legacy color, let space be hsl.

    • Otherwise, throw an error.

  • Otherwise:

    • If $space is not a known color space with a polar-angle hue channel, throw an error.

      This currently allows hsl, hwb, lch, and oklch. We may decide to provide additional options in the future.

    • Let space be the value of $space.

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

color.invert()

invert($color, $space: null)

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

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

  • If $space is null:

    • If $color is a legacy color, let space be rgb, and let mix-space be null.

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

    • Otherwise, throw an error.

  • Otherwise:

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

    • Let space be $space, and let mix-space be $space.

  • Let color be the result of converting and gamut mapping $color into the color space space.

  • If space == hwb:

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

    • Let hue-out be the result of (hue + 180deg) % 360deg.

    • Let gray be the result of whiteness + blackness.

    • Let white be gray - whiteness, and let black be gray - blackness.

    • Let invert be the result of calling color.change(color, $hue: hue-out, $whiteness: white, $blackness: black).

  • Otherwise:

    • Let invert be the value of color.

    • For each channel element in color's channels:

      • If channel represents a polar-angle hue:

        • Let new be (channel + 180deg) % 360deg.
      • Otherwise, if channel represents either chroma or saturation:

        • Let new be channel.
      • Otherwise:

        • Let min and max be the minimum and maximum values defined for channel in space.

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

      • Set the corresponding channel of invert to be new.

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

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

  • If $color is a legacy color:

    • Return the result of converting $color to hsl, and changing the 'saturation' channel to 0.
  • Otherwise:

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 $color to rgb.

  • 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 as the next item in hex-list.

  • Let alpha be rgb's alpha value.

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

  • Append hex-alpha as the next item in 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 components be the result of parsing $channels with an hwb space.

    • If components is null, return a plain CSS function string with the name "hwb" and the argument $channels.

    • Let channels be the first element and alpha the second element of components.

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

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

    • Return a legacy color in the hwb space, with the given hue, whiteness, and blackness channels, and alpha value.

lab()

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

    • If components is null, return a plain CSS function string with the name "lab" and the argument $channels.

    • Let channels be the first element and alpha the second element of components.

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

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

lch()

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

    • If components is null, return a plain CSS function string with the name "lab" and the argument $channels.

    • Let channels be the first element and alpha the second element of components.

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

    • Return a color in the lch known color space, with the given lightness, chroma, and hue channels, and alpha value.

oklab()

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

    • If components is null, return a plain CSS function string with the name "lab" and the argument $channels.

    • Let channels be the first element and alpha the second element of components.

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

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

oklch()

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

    • If components is null, return a plain CSS function string with the name "lab" and the argument $channels.

    • Let channels be the first element and alpha the second element of components.

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

    • Return a color in the oklch known color space, with the given lightness, chroma, and hue channels, and alpha value.

color()

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

    • If components is null, return a plain CSS function string with the name "color" and the argument $description.

    • Let color be the first element and alpha the second element of components.

    • Let space be the first element and channels the second element of color.

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

  • rgb($red, $green, $blue, $alpha: 1)
    
    • 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 color space.

    • Return a legacy color in the rgb space, 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 components be the result of parsing $channels with an rgb space.

    • If components is null, return a plain CSS function string with the name "rgb" and the argument $channels.

    • Let channels be the first element and alpha the second element of components.

    • 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 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 hsl color space.

    Clamping and conversion to rgb have been removed.

    • Return a legacy color in the hsl space, 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 components be the result of parsing $channels with an hsl space.

    • If components is null, return a plain CSS function string with the name "hsl" and the argument $channels.

    • Let channels be the first element and alpha the second element of components.

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

    • Return a legacy color in the hsl space, with the given hue, saturation, and lightness channels, and alpha value.