CSS Color Module Level 4

W3C Working Draft,

More details about this document
This version:
https://www.w3.org/TR/2022/WD-css-color-4-20220428/
Latest published version:
https://www.w3.org/TR/css-color-4/
Editor's Draft:
https://drafts.csswg.org/css-color/
Previous Versions:
History:
https://www.w3.org/standards/history/css-color-4
Test Suites:
http://test.csswg.org/suites/css-color-4_dev/nightly-unstable/
https://wpt.fyi/results/css/css-color/
Feedback:
CSSWG Issues Repository
Inline In Spec
Editors:
Tab Atkins Jr. (Google)
Chris Lilley (W3C)
Lea Verou (Invited Expert)
Former Editor:
L. David Baron (Mozilla)
Suggest an Edit for this Spec:
GitHub Editor

Abstract

This specification describes CSS <color> values and properties for foreground color and group opacity.

CSS is a language for describing the rendering of structured documents (such as HTML and XML) on screen, on paper, etc.

Status of this document

This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C technical reports index at https://www.w3.org/TR/.

This document was published by the CSS Working Group as a Working Draft using the Recommendation track. Publication as a Working Draft does not imply endorsement by W3C and its Members.

This is a draft document and may be updated, replaced or obsoleted by other documents at any time. It is inappropriate to cite this document as other than work in progress.

Please send feedback by filing issues in GitHub (preferred), including the spec code “css-color” in the title, like this: “[css-color] …summary of comment…”. All issues and comments are archived. Alternately, feedback can be sent to the (archived) public mailing list www-style@w3.org.

This document is governed by the 2 November 2021 W3C Process Document.

This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent which the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy.

The following features are at-risk, and may be dropped during the CR period:

“At-risk” is a W3C Process term-of-art, and does not necessarily imply that the feature is in danger of being dropped or delayed. It means that the WG believes the feature may have difficulty being interoperably implemented in a timely manner, and marking it as such allows the WG to drop the feature if necessary when transitioning to the Proposed Rec stage, without having to publish a new Candidate Rec without the feature first.

1. Introduction

This section is not normative.

This module describes CSS properties which allow authors to specify the foreground color and opacity of the text content of an element. This module also describes in detail the CSS <color> value type.

It not only defines the color-related properties and values that already exist in CSS1, CSS2, and CSS Color 3, but also defines new properties and values.

In particular, it allows specifying colors in other color spaces than sRGB; previously, the more saturated colors outside the sRGB gamut could not be used in CSS even if the display device supported them. In addition to the family of RGB color spaces, color spaces with other primaries such as CMYK or KCMYGOV are supported.

A draft implementation report is available.

1.1. Value Definitions

This specification follows the CSS property definition conventions from [CSS2] using the value definition syntax from [CSS-VALUES-3]. Value types not defined in this specification are defined in CSS Values & Units [CSS-VALUES-3]. Combination with other CSS modules may expand the definitions of these value types.

In addition to the property-specific values listed in their definitions, all properties defined in this specification also accept the CSS-wide keywords as their property value. For readability they have not been repeated explicitly.

2. Color terminology

A color is a definition (numeric or textual) of the human visual perception of a light or a physical object illuminated with light. The objective study of human color perception is termed colorimetry.

The color of a physical object depends on how much light it reflects at each visible wavelength, plus the actual color of the light illuminating it (again, the amount of light at each wavelength). It is measured by a spectrophotometer .

The color of something that emits light (including colors on a computer screen) depends on how much light it emits at each visible wavelength. It is measured by a spectroradiometer.

If two objects have different spectra, but still produce the same physical sensation, we say they have the same color. We can calculate whether two colors are the same by converting the spectra to CIE XYZ (three numbers).

For example a green leaf, a photograph of that leaf displayed on a computer screen, and a print of that photograph, are all producing a green sensation by different means. If the screen and the printer are calibrated, the green in the leaf, and the photo, and the print will look the same.

A color space is an organization of colors with respect to an underlying colorimetric model, such that there is a clear, objectively-measurable meaning for any color in that color space. This also means that the same color can be expressed in multiple color spaces, or transformed from one color space to another, while still looking the same.

A leaf is measured with a spectrophotometer and found to have the color lch(51.2345% 21.2 130) which is lab(51.2345% -13.6271 16.2401).

This same color could be expressed in various color spaces:

 color(sRGB 0.41587 0.503670 0.36664);
 color(display-p3 0.43313 0.50108 0.37950);
 color(a98-rgb 0.44091 0.49971 0.37408);
 color(prophoto-rgb 0.36589 0.41717 0.31333);
 color(rec2020 0.42210 0.47580 0.35605);

An additive color space means that the coordinate system is linear in light intensity. The CIE XYZ color space is an additive color space (in addition, the Y component of XYZ is the luminance).

In an additive color space, calculations can be done to accurately predict color mixing. Most RGB spaces are not additive, because the components are gamma encoded. Undoing this gamma encoding produces linear-light values.

For example, if a light fixture contains two identical colored lights, and only one is switched on, and the color is measured to be color(xyz 0.13 0.12 0.04), then the color when both are switched on will be exactly twice that, color(xyz 0.26 0.24 0.08).

If we have two differently colored spotlights shining on a stage, and one has the measured value color(xyz 0.15 0.24 0.17) while the other is color(xyz 0.11 0.06 0.06) then we can accurately predict that if the colored beams are made to overlap, the color of the mixture will be the sum of the XYZ component values, or color(xyz 0.26 0.30 0.23).

A chromaticity is a color measurement where the lightness component has been factored out. From the identical lights example above, the u',v' chromaticity with one light is (0.2537, 0.5268) and the chromaticity is the same with both lights (they are the same color, it is just brighter).

Chromaticities are additive, so they accurately predict the chromaticity (but not the resulting lightness) of a mixture. Being two-dimensional, chromaticity is easily represented on a chromaticity diagram to predict the chromaticity of a color mixture. Any two colors can be mixed, and the resulting colors will lie on the line joining them on the diagram. Three colors form a plane, and the resulting colors will lie in the triangle they form on the diagram.

uv chromaticity diagram of the display-p3 color space
A chromaticity diagram showing (in solid colors) the display-p3 color space and for comparison (faded) the sRGB color space. The white point (D65) is also shown.

RGB color spaces are additive, and their gamut is defined by the chromaticities of the red, green and blue primaries, plus the chromaticity of the white point (the color formed by all three primaries at full intensity).

Most color spaces use one of a few daylight-simulating white points, which are named by the color temperature of the corresponding black-body radiator. For example, D65 is a daylight whitepoint corresponding to a correlated color temperature of 6500 Kelvin (actually 6504, because the value of Plank’s constant has changed since the color was originally defined).

To avoid cumulative round-trip errors, it is important that the identical chromaticity values are used consistently, at all places in a calculation. Thus, for maximum compatibility, for this specification, the following two standard daylight-simulating white points are defined:

Name  x y CCT
D50 0.345700 0.358500 5003K
D65 0.312700 0.329000 6504K

When the measured physical characteristics (such as the chromaticities of the primary colors it uses, or the colors produced in response to a given set of inputs) of a color space or a color-producing device are known, it is said to be characterized.

If in addition adjustments have been made so that a device meets calibration targets such as white point, neutrality of greys, predictability and consistency of tone response, then it is said to be calibrated.

Real physical devices cannot yet produce every possible color that the human eye can see. The range of colors that a given device can produce is termed the gamut (not to be confused with gamma). Devices with a limited gamut cannot produce very saturated colors, like those found in a rainbow.

The gamuts of different color spaces may be compared by looking at the volume (in cubic Lab units) of colors that can be expressed. The following table examines the predefined color spaces available in CSS.

color space Volume (million Lab units)
sRGB 0.820
display-p3 1.233
a98-rgb 1.310
prophoto-rgb 2.896
rec2020 2.042

3. Foreground Color: the color property

Name: color
Value: <color>
Initial: CanvasText
Applies to: all elements and text
Inherited: yes
Percentages: N/A
Computed value: computed color, see resolving color values
Canonical order: per grammar
Animation type: by computed value type
Tests

This property describes the foreground fill color of an element’s text content. In addition, it provides the value that currentcolor resolves to.

There are several different ways to syntactically specify a given color.

For example, to specify the sRGB color lime green:
em { color:  lime; }   /* color keyword  */
em { color:  rgb(0 255 0); } /* RGB range 0-255   */
em { color:  rgb(0% 100% 0%); } /* RGB range 0%-100% */
em { color:  color(sRGB 0 1 0); } /* sRGB range 0.0-1.0 */
<color>
The <color> type is defined in a later section.

Note: In general, this property, including its alpha component, has no effect on "color glyphs", such as emoji in some fonts, which are colored by a built-in palette. Some colored fonts are able to refer to the foreground color, such as palette entry 0xFFFF in COLR table of OpenType, and context-fill value in SVG-in-OpenType. In that case, the foreground color is set by this property, identical to how currentcolor value works.

4. Representing Colors: the <color> type

CSS has several syntaxes for specifying color values. Some directly specify an sRGB color by its channels, such as the hex color notation or rgb() function. Others are more human-friendly to write and understand, such as the hsl() and lch() functions, or the long list of named colors.

4.1. The <color> syntax

Colors are represented as a list of components, also sometimes called “channels”, representing axises in the color space. Each channel has a minimum and maximum value, and can take any value between those two. Additionally, every color is accompanied by an alpha component, indicating how transparent it is, and thus how much of the backdrop one can see behind the color.

Colors in CSS are represented by the <color> type:

<color> = <absolute-color-base> | currentcolor | <system-color> | <device-cmyk()>

<absolute-color-base> = <hex-color> | <named-color> | transparent |
    <rgb()> | <rgba()> | <hsl()> | <hsla()> | <hwb()> |
    <lab()> | <lch()> |
    <oklab()> | <oklch()> |
    <color()>

The color-functions are <rgb()>, <rgba()>, <hsl()>, <hsla()>, <hwb()>, <lab()>, <lch()>, <oklab()>, <oklch()>, and <color()>.

Of those, <hsl()>, <hsla()>, <hwb()>, <lch()>, and <oklch()> are cylindrical polar color representations, which specify color using a <hue> angle, a central axis representing lightness (black-to-white), and a radius representing saturation or chroma (how far the color is from a neutral grey). The other color spaces are rectangular orthogonal color representations, which specify color using three (or more, in the case of CMYK or CMYKOGV) orthogonal component axes.

For easy reference in other specifications, opaque black is defined as the color rgb(0 0 0 / 100%); transparent black is the same color, but fully transparent—i.e. rgb(0 0 0 / 0%).

All of the <color> syntactic forms defined in this specification use space, not comma, to separate the color components. They also use a solidus ("/") to separate an optional alpha term from the color components.

For Web compatibility, <color> syntactic forms defined in earlier levels of this specification also support legacy color syntax which has the following differences:

For the syntactic forms introduced in this or subsequent levels, where there is no Web compatibility issue, legacy color syntax is not supported, and attempting to use it is an error.

4.2. The <hue> syntax

Hue is represented as an angle of the color circle (the rainbow, twisted around into a circle, and with purple added between violet and red).

<hue> = <number> | <angle> | none

Because this value is so often given in degrees, the argument can also be given as a number, which is interpreted as a number of degrees.

This number is not constrained to the range [0,360] but is unbounded. Certain operations, such as hue interpolation, may normalize the hue angle during calculations.

The angles and spacing corresponding to particular hues depend on the color space. For example, in HSL and HWB, which use the sRGB color space, sRGB green is 120 degrees. In LCH, sRGB green is 134.39 degrees, display-p3 green is 136.01 degrees, a98-rgb green is 145.97 degrees and prophoto-rgb green is 141.04 degrees (because these are all different shades of green).

<hue> components are the most common components to become powerless; any achromatic color will have a powerless hue component.

4.3. "Missing"/none Color Components

In certain cases, a color can have one or more missing color components.

In this specification, this happens automatically due to hue-based interpolation for some colors (such as white); other specifications can define additional situations in which components are automatically missing.

It can also be specified explicitly, by providing the keyword none for a component in a color function. All color functions allow any of their components to be specified as none.

For handling of missing component in color interpolation, see § 12.2 Interpolating with missing components

For all other purposes, a missing component behaves as a zero value, in the appropriate unit for that component: 0, 0%, or 0deg. This includes rendering the color directly, converting it to another color space, performing computations on the color component values, etc.

If a color with a missing component is serialized or otherwise presented directly to an author, then for legacy color syntax it represents that component as a zero value; otherwise, it represents that component as being the none keyword.

A missing hue is common when interpolating in cylindrical color spaces. For example, using the color-mix() function specified in [CSS-COLOR-5] one could write color-mix(in hsl, white 30%, green 70%). Since white is an achromatic color, it has a missing hue when expressed in hsl() (effectively hsl(none none 100%)), since any hue or saturation will produce the same color) which means that the color-mix function will treat it as having the same hue and saturation as green (effectively hsl(120deg 100% 100%)), and then interpolate based on those components.

The result will be a color that truly looks like a blend of green and white, rather than perhaps looking reddish (if whites hue was defaulted to 0deg) or grayish (if whites saturation was defaulted to 0%).

Explicitly specifying missing components can be useful to achieve an effect where you only want to interpolate certain components of a color.

For example, to animate a color to "grayscale", no matter what the color is, one can interpolate it with lch(none 0 none). This will take the hue and lightness from the starting color, but animate its chroma down to 0, rendering it into an equal-lightness gray with a steady hue across the whole animation.

Doing this manually would require matching the hue and lightness of the starting color explicitly.

4.3.1. "Powerless" Color Components

Individual color syntaxes can specify that, in some cases, a given component of their syntax becomes a powerless color component. This indicates that the value of the component doesn’t affect the rendered color; any value you give it will result in the same color displayed in the screen.

For example, in hsl(), the hue component is powerless when the saturation component is 0%; a 0% saturation indicates a grayscale color, which has no hue at all, so 0deg and 180deg, or any other angle, will give the exact same result.

If a powerless component is manually specified, it acts as normal; the fact that it’s powerless has no effect. However, if a color is automatically produced by color space conversion, then any powerless components in the result must instead be set to missing, instead of whatever value was produced by the conversion process.

User agents may treat a component as powerless if the color is "sufficiently close" to the precise conditions specified. For example, a gray color converted into lch() may, due to numerical errors, have an extremely small chroma rather than precisely 0%; this can, at the user agent’s discretion, still treat the hue component as powerless. It is intentionally unspecified exactly what "sufficiently close" means for this purpose.

4.4. Accessibility and Conveying Information By Color

Although colors can add significant information to documents and make them more readable, color by itself should not be the sole means to convey important information. Authors should consider the W3C Web Content Accessibility Guidelines [WCAG21] when using color in their documents.

1.4.1 Use of Color: Color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element

4.5. Color Space of Tagged Images

An tagged image is an image that is explicitly assigned a color profile, as defined by the image format. This is usually done by including an International Color Consortium (ICC) profile [ICC].

For example JPEG [JPEG], PNG [PNG] and TIFF [TIFF] all specify a means to embed an ICC profile.

Image formats may also use other, equivalent methods, often for brevity.

For example, PNG specifies a means (the sRGB chunk) to explicitly tag an image as being in the sRGB color space, without including the sRGB ICC profile.

Tagged RGB images, and tagged images using a transformation of RGB such as YCbCr, if the color profile or other identifying information is valid, must be treated as being in the specified color space.

Tests

If the color profile or other identifying information is invalid, the image is treated as untagged images

For example, when a browser running on a system with a Display P3 monitor displays an JPEG image tagged as being in the ITU Rec BT.2020 [Rec.2020] color space, it must convert the colors from ITU Rec BT.2020 to Display P3 so that they display correctly. It must not treat the ITU Rec BT.2020 values as if they were Display P3 values, which would produce incorrect colors.

4.6. Color Spaces of Untagged Colors

Colors specified in HTML, and untagged images must be treated as being in the sRGB color space ([SRGB]) unless otherwise specified.

Tests

An untagged image is an image that is not explicitly assigned a color profile, as defined by the image format.

Note that this rule does not apply to untagged videos, since untagged video should be presumed to be in an ITU-defined color space.

4.7. Resolving <color> Values

Unless otherwise specified for a particular property, specified colors are resolved to computed colors and then further to used colors as described below.

The resolved value of a <color> is its used value.

Tests

4.7.1. Resolving sRGB values

This applies to:

Tests

For example, the first form below is treated as identical to the second:

 HsL(39 100% 50%)
 hsl(39 100% 50%)

Similarly the first form below is treated as identical to the second:

 pUrPlE
 purple

The computed and used value is the corresponding sRGB color, paired with the specified alpha channel and defaulting to opaque if unspecified).

For example, the computed value of

 hsl(38.824 100% 50%)

is

 rgb(255, 165, 0)

4.7.2. Resolving Lab and LCH values

This applies to lab() and lch() values.

The computed and used value is the corresponding CIE Lab or LCH color paired with the specified alpha channel (as a <number>, not a <percentage>; and defaulting to opaque if unspecified).

For example, the computed value of

 lch(52.2345% 72.2 56.2 / 1)

is

 lch(52.2345% 72.2 56.2)

4.7.3. Resolving OKLab and OKLCH values

This applies to oklab() and oklch() values.

The computed and used value is the corresponding OKLab or OKLCH color paired with the specified alpha channel (as a <number>, not a <percentage>; and defaulting to opaque if unspecified).

For example, the computed value of

 oklch(42.1% 0.192 328.6 / 1)

is

 oklch(42.1% 0.192 328.6)

4.7.4. Resolving values of the color() function

The computed and used value is the color in the specified color space, paired with the specified alpha channel (as a <number>, not a <percentage>; and defaulting to opaque if unspecified).

For example, the computed value of

 color(display-p3 0.823 0.6554 0.2537 /1)

is

 color(display-p3 0.823 0.6554 0.2537)

For colors specified in the xyz color space, which is an alias of the xyz-d65 color space, the computed and used value is in the xyz-d65 color space.

For example, the computed value of

 color(xyz 0.472 0.372 0.131)

is

 color(xyz-d65 0.472 0.372 0.131)

4.7.5. Resolving other colors

This applies to system colors (including the <deprecated-color>s), transparent, and currentcolor.

Each <system-color> keyword computes to the corresponding color in its color space. However, such colors must not be altered by 'forced colors mode'.

The computed and used value of transparent is transparent black.

The currentcolor keyword computes to itself.

In the color property, the used value of currentcolor is the inherited value. In any other property, its used value is the used value of the color property on the same element.

Note: This means that if the currentcolor value is inherited, it’s inherited as a keyword, not as the value of the color property, so descendants will use their own color property to resolve it.

4.8. Serializing <color> Values

This section updates and replaces that part of CSS Object Model, section Serializing CSS Values, which relates to serializing <color> values.

In this section, the strings used in the specification and the corresponding characters are as follows.

String Character(s)
" " U+0020 SPACE
"," U+002C COMMA
"-" U+002D HYPHEN-MINUS
"." U+002E FULL STOP
"/" U+002F SOLIDUS
"none" U+006E LATIN SMALL LETTER N
U+006F LATIN SMALL LETTER O
U+006E LATIN SMALL LETTER N
U+0065 LATIN SMALL LETTER E

The string "." shall be used as a decimal separator, regardless of locale, and there shall be no thousands separator.

For syntactic forms which support missing color components, the value none (equivalently NONE, nOnE, etc), shall be serialized in all-lower case as the string "none".

4.8.1. Serializing alpha values

This applies to any <color> value which can take an optional alpha value.

If the alpha is exactly 1, it is omitted from the serialization; an implicit value of 1 (fully opaque) is the default.

If the alpha is any other value than 1, it is explicitly included in the serialization, as a <number>, not a <percentage>. The value is expressed in base ten, with the "." character as decimal separator. The leading zero must not be omitted. Trailing zeroes must be omitted.

For example, an alpha value of 70% will be serialized as the string "0.7" which has a leading zero before the decimal separator, "." as decimal separator (even if the current locale would use some other character, such as ","), and all digits after the "7" would be "0" and are omitted.

The precision with which alpha values are retained, and thus the number of decimal places in the serialized value, is not defined in this specification, but must at least be sufficient to round-trip integer percentage values. Thus, the serialized value must contain at least two decimal places (unless trailing zeroes have been removed). Values must be rounded towards +∞, not truncated.

For example, an alpha value of 12.3456789% could be serialized as the strings "0.12" or "0.123" or "0.1234" or "0.12346" (rounding the value of 5 towards +∞ because the following digit is 6) or any longer, rounded serialization of the same form.

4.8.2. Serializing sRGB values

The serialized form of the following sRGB values:

is derived from the computed value and thus, uses either the rgb() or rgba() form (depending on whether the alpha is exactly 1, or not), with lowercase letters for the function name.

During serialization, any missing values are converted to 0.

For compatibility, the sRGB component values are serialized in <number> form, not <percentage>). Also for compatibility, the component values are serialized in base 10, with a range of [0-255], regardless of the bit depth with which they are stored.

As noted earlier, unitary alpha values are not explicitly serialized. Also, for compatibility, if the alpha is exactly 1, the rgb() form is used, with an implicit alpha; otherwise, the rgba() form is used, with an explicit alpha value.

For compatibility, the legacy form with comma separators is used; exactly one ASCII space follows each comma. This includes the comma (not slash) used to separate the blue component of rgba() from the alpha value.

For example, the serialized value of

 rgb(29 164 192 / 95%)

is the string "rgba(29, 164, 192, 0.95)"

Note: contrary to CSS Color 3, the parameters of the rgb() function are of type <number> not <integer>. Thus, any higher precision than eight bits is indicated with a fractional part.

Trailing fractional zeroes in any component values must be omitted; if the fractional part consists of all zeroes, the decimal point must also be omitted. This means that sRGB colors specified with integer component values will serialize with backwards-compatible integer values.

The precision with which sRGB component values are retained, and thus the number of significant figures in the serialized value, is not defined in this specification, but must at least be sufficient to round-trip eight bit values. Values must be rounded towards +∞, not truncated.

Note: authors of scripts which expect color values returned from getComputedStyle to have <integer> component values, are advised to update them to cope with <number>.

For example,

 rgb(146.064 107.457 131.223)

is now valid, and equal to

 rgb(57.28% 42.14% 51.46%)

A conformant serialized form for both, is the string "rgb(146.06, 107.46, 131.2)".

The serialized value of

 goldenrod

is the string "rgb(218, 165, 32)" and not the string "rgb(218.000, 165.000, 32.000)"

4.8.3. Serializing Lab and LCH values

The serialized form of lch() and lab() values is derived from the computed value and uses the lab() or lch() forms, with lowercase letters for the function name.

The component values are serialized in base 10; the Lightness, a and b component values are serialized as <number> with 0% Lightness mapping to 0 and 100% Lightness mapping to 100. A single ASCII space character " " must be used as the separator between the component values.

Trailing fractional zeroes in any component values must be omitted; if the fractional part consists of all zeroes, the decimal point must also be omitted.

Tests

The serialized value of

 lab(56.200% 0.000 83.600)

is the string "lab(56.2 0 83.6)"

The serialized value of

 lch(37% 105.0 305.00)

is the string "lch(37 105 305)", not "lch(37 105.0 305.00)".

The precision with which lab() component values are retained, and thus the number of significant figures in the serialized value, is not defined in this specification, but due to the wide gamut must be sufficient to round-trip L values between 0 and 100, and a and b values between ±127, with at least sixteen bit precision; this will result in at least three decimal places unless trailing zeroes have been omitted. (half float or float, is recommended for internal storage). Values must be rounded towards +∞, not truncated.

Note: a and b values outside ±127 are possible with ultrawide gamut spaces. For example, all of the prophoto-rgb primaries and secondaries exceed this range, but are within ±200.

As noted earlier, unitary alpha values are not explicitly serialized. Non-unitary alpha values must be explicitly serialized, and the string " / " (an ASCII space, then forward slash, then another space) must be used to separate the b component value from the alpha value.

The serialized value of

 lch(56.2% 83.6 357.4 /93%)

is the string "lch(56.2 83.6 357.4 / 0.93)" not "lch(56.2% 83.6 357.4 / 0.93)"

4.8.4. Serializing OKLab and OKLCH values

The serialized form of oklch() and oklab() values is derived from the computed value and uses the oklab() or oklch() forms, with lowercase letters for the function name.

The component values are serialized in base 10; the Lightness, a and b component values are serialized as <number> with 0% Lightness mapping to 0 and 100% Lightness mapping to 1. A single ASCII space character " " must be used as the separator between the component values.

Trailing fractional zeroes in any component values must be omitted; if the fractional part consists of all zeroes, the decimal point must also be omitted.

Tests

The serialized value of

 oklab(54.0% -0.10 -0.02)

is the string "oklab(0.54 -0.1 -0.02)" not "oklab(54 -0.1 -0.02)" or "oklab(54% -0.1 -0.02)"

The serialized value of

 oklch(56.43% 0.0900 123.40)

is the string "oklch(0.5643 0.09 123.4)", not "oklch(0.5643 0.0900 123.40)".

The precision with which oklab() component values are retained, and thus the number of significant figures in the serialized value, is not defined in this specification, but due to the wide gamut must be sufficient to round-trip L values between 0 and 1 (0% and 100%), and a and b values between ±0.5, with at least sixteen bit precision; this will result in at least five decimal places unless trailing zeroes have been omitted. (half float or float, is recommended for internal storage). Values must be rounded towards +∞, not truncated.

Note: a and b values outside ±0.5 are possible with ultrawide gamut spaces. For example, the prophoto-rgb green and blue primaries exceed this range, with chroma of 0.526 and 1.413 respectively.

As noted earlier, unitary alpha values are not explicitly serialized. Non-unitary alpha values must be explicitly serialized, and the string " / " (an ASCII space, then forward slash, then another space) must be used to separate the b component value from the alpha value.

The serialized value of

 oklch(53.85% 0.1725 320.67 / 70%)

is the string "oklch(53.85% 0.1725 320.67 / 0.7)"

4.8.5. Serializing values of the color() function

The serialized form of color() values is derived from the computed value and uses the color() form, with lowercase letters for the function name and the color space name.

The component values are serialized in base 10, as <number>. A single ASCII space character " " must be used as the separator between the component values, and also between the color space name and the first color component.

Trailing fractional zeroes in any component values must be omitted; if the fractional part consists of all zeroes, the decimal point must also be omitted.

Tests

The serialized value of

 color(dIsPlAy-P3  0.964  0.763  0.787)

is the string "color(display-p3 0.96 0.76 0.79)", if two decimal places are retained. Notice that 0.787 has rounded up to 0.79, rather than being truncated to 0.78.

If the color space is sRGB, the color space is still explicitly required in the serialized result.

For the predefined color spaces, the minimum precision for round-tripping is as follows:

color space Minimum bits
srgb 10
srgb-linear 12
display-p3 10
a98-rgb 10
prophoto-rgb 12
rec2020 12
xyz, xyz-d50, xyz-d65 16

(16bit, half-float, or float per component is recommended for internal storage). Values must be rounded towards +∞, not truncated.

Note: compared to the legacy forms such as rgb(), hsl() and so on, color(srgb) has a higher minimum precision requirement. Stylesheet authors who prefer higher precision are thus encouraged to use the color(srgb) form.

As noted earlier, unitary alpha values are not explicitly serialized. Non-unitary alpha values must be explicitly serialized, and the string " / " (an ASCII space, then forward slash, then another space) must be used to separate the final color component value from the alpha value.

The serialized value of

 color(prophoto-rgb 0.2804 0.40283 0.42259/85%)

is the string "color(prophoto-rgb 0.28 0.403 0.423 / 0.85)", if three decimal places are retained.

4.8.6. Serializing other colors

This applies to transparent and currentcolor.

The serialized form of these values is derived from the computed value and uses lowercase letters for the color name.

The serialized form of transparent is the string "rgba(0, 0, 0, 0)".

The serialized form of currentColor is the string "currentcolor".

5. sRGB Colors

CSS colors in the sRGB color space are represented by a triplet of values—red, green, and blue—identifying a point in the sRGB color space [SRGB]. This is an internationally-recognized, device-independent color space, and so is useful for specifying colors that will be displayed on a computer screen, but is also useful for specifying colors on other types of devices, like printers.

CSS also allows the use of non-sRGB color spaces, as described in § 10 Predefined Color Spaces.

CSS provides several methods of directly specifying an sRGB color: hex colors, rgb()/rgba() color-functions, hsl()/hsla() color-functions, hwb() color-function, named colors, and the transparent keyword.

5.1. The RGB functions: rgb() and rgba()

The rgb() function defines an sRGB color by specifying the red, green, and blue channels directly. Its syntax is:

rgb() = rgb( [<percentage> | none]{3} [ / [<alpha-value> | none] ]? ) |
  rgb( [<number> | none]{3} [ / [<alpha-value> | none] ]? )
<alpha-value> = <number> | <percentage>
Tests

The first three arguments specify the red, green, and blue channels of the color, respectively. 0% represents the minimum value for that color channel in the sRGB gamut, and 100% represents the maximum value. A <number> is equivalent to a <percentage>, but with a different range: 0 again represents the minimum value for the color channel, but 255 represents the maximum. These values come from the fact that many graphics engines store the color channels internally as a single byte, which can hold integers between 0 and 255. Implementations should honor the precision of the channel as authored or calculated wherever possible. If this is not possible, the channel should be rounded towards +∞.

The final argument, the <alpha-value>, specifies the alpha of the color. If given as a <number>, the useful range of the value is 0 (representing a fully transparent color) to 1 (representing a fully opaque color). If given as a <percentage>, 0% represents a fully transparent color, while 100% represents a fully opaque color. If omitted, it defaults to 100%.

Tests

Values outside these ranges are not invalid, but are clamped to the ranges defined here at computed-value time.

For legacy reasons, rgb() also supports a legacy color syntax that separates all of its arguments with commas:

rgb() = rgb( <percentage>#{3} , <alpha-value>? ) |
  rgb( <number>#{3} , <alpha-value>? )

Also, an rgba() legacy color syntax also exists, with an identical grammar and behavior to rgb().

Tests

5.2. The RGB hexadecimal notations: #RRGGBB

The CSS hex color notation allows an sRGB color to be specified by giving the channels as hexadecimal numbers, which is similar to how colors are often written directly in computer code. It’s also shorter than writing the same color out in rgb() notation.

The syntax of a <hex-color> is a <hash-token> token whose value consists of 3, 4, 6, or 8 hexadecimal digits. In other words, a hex color is written as a hash character, "#", followed by some number of digits 0-9 or letters a-f (the case of the letters doesn’t matter - #00ff00 is identical to #00FF00).

The number of hex digits given determines how to decode the hex notation into an RGB color:

6 digits
The first pair of digits, interpreted as a hexadecimal number, specifies the red channel of the color, where 00 represents the minimum value and ff (255 in decimal) represents the maximum. The next pair of digits, interpreted in the same way, specifies the green channel, and the last pair specifies the blue. The alpha channel of the color is fully opaque.
In other words, #00ff00 represents the same color as rgb(0 255 0) (a lime green).
8 digits
The first 6 digits are interpreted identically to the 6-digit notation. The last pair of digits, interpreted as a hexadecimal number, specifies the alpha channel of the color, where 00 represents a fully transparent color and ff represent a fully opaque color.
In other words, #0000ffcc represents the same color as rgb(0 0 100% / 80%) (a slightly-transparent blue).
3 digits
This is a shorter variant of the 6-digit notation. The first digit, interpreted as a hexadecimal number, specifies the red channel of the color, where 0 represents the minimum value and f represents the maximum. The next two digits represent the green and blue channels, respectively, in the same way. The alpha channel of the color is fully opaque.
This syntax is often explained by saying that it’s identical to a 6-digit notation obtained by "duplicating" all of the digits. For example, the notation #123 specifies the same color as the notation #112233. This method of specifying a color has lower "resolution" than the 6-digit notation; there are only 4096 possible colors expressible in the 3-digit hex syntax, as opposed to approximately 17 million in 6-digit hex syntax.
4 digits
This is a shorter variant of the 8-digit notation, "expanded" in the same way as the 3-digit notation is. The first digit, interpreted as a hexadecimal number, specifies the red channel of the color, where 0 represents the minimum value and f represents the maximum. The next three digits represent the green, blue, and alpha channels, respectively.
Tests

6. Color Keywords

In addition to the various numeric syntaxes for <color>s, CSS defines several sets of color keywords that can be used instead—each with their own advantages or use cases.

6.1. Named Colors

CSS defines a large set of named colors, so that common colors can be written and read more easily. A <named-color> is written as an <ident>, accepted anywhere a <color> is. As usual for CSS-defined <ident>s, all of these keywords are ASCII case-insensitive.

The names resolve to colors in sRGB.

16 of CSS’s named colors come from the VGA palette originally, and were then adopted into HTML: aqua, black, blue, fuchsia, gray, green, lime, maroon, navy, olive, purple, red, silver, teal, white, and yellow. Most of the rest come from one version of the X11 color system, used in Unix-derived systems to specify colors for the console, and were then adopted into SVG.

Note: these color names are standardized here, not because they are good, but because their use and implementation has been widespread for decades and the standard needs to reflect reality. Indeed, it is often hard to imagine what each name will look like (hence the list below); the names are not evenly distributed throughout the sRGB color volume, the names are not even internally consistent ( darkgray is lighter than gray, while lightpink is darker than pink), and some names (such as indianred, which was originally named after a red pigment from India), have been found to be offensive. Thus, their use is not encouraged.

(Two special color values, transparent and currentcolor, are specially defined in their own sections.)

The following table defines all of the opaque named colors, by giving equivalent numeric specifications in the other color syntaxes.

Named Numeric Color name Hex rgb Decimal
aliceblue #f0f8ff 240 248 255
antiquewhite #faebd7 250 235 215
aqua #00ffff 0 255 255
aquamarine #7fffd4 127 255 212
azure #f0ffff 240 255 255
beige #f5f5dc 245 245 220
bisque #ffe4c4 255 228 196
black #000000 0 0 0
blanchedalmond #ffebcd 255 235 205
blue #0000ff 0 0 255
blueviolet #8a2be2 138 43 226
brown #a52a2a 165 42 42
burlywood #deb887 222 184 135
cadetblue #5f9ea0 95 158 160
chartreuse #7fff00 127 255 0
chocolate #d2691e 210 105 30
coral #ff7f50 255 127 80
cornflowerblue #6495ed 100 149 237
cornsilk #fff8dc 255 248 220
crimson #dc143c 220 20 60
cyan #00ffff 0 255 255
darkblue #00008b 0 0 139
darkcyan #008b8b 0 139 139
darkgoldenrod #b8860b 184 134 11
darkgray #a9a9a9 169 169 169
darkgreen #006400 0 100 0
darkgrey #a9a9a9 169 169 169
darkkhaki #bdb76b 189 183 107
darkmagenta #8b008b 139 0 139
darkolivegreen #556b2f 85 107 47
darkorange #ff8c00 255 140 0
darkorchid #9932cc 153 50 204
darkred #8b0000 139 0 0
darksalmon #e9967a 233 150 122
darkseagreen #8fbc8f 143 188 143
darkslateblue #483d8b 72 61 139
darkslategray #2f4f4f 47 79 79
darkslategrey #2f4f4f 47 79 79
darkturquoise #00ced1 0 206 209
darkviolet #9400d3 148 0 211
deeppink #ff1493 255 20 147
deepskyblue #00bfff 0 191 255
dimgray #696969 105 105 105
dimgrey #696969 105 105 105
dodgerblue #1e90ff 30 144 255
firebrick #b22222 178 34 34
floralwhite #fffaf0 255 250 240
forestgreen #228b22 34 139 34
fuchsia #ff00ff 255 0 255
gainsboro #dcdcdc 220 220 220
ghostwhite #f8f8ff 248 248 255
gold #ffd700 255 215 0
goldenrod #daa520 218 165 32
gray #808080 128 128 128
green #008000 0 128 0
greenyellow #adff2f 173 255 47
grey #808080 128 128 128
honeydew #f0fff0 240 255 240
hotpink #ff69b4 255 105 180
indianred #cd5c5c 205 92 92
indigo #4b0082 75 0 130
ivory #fffff0 255 255 240
khaki #f0e68c 240 230 140
lavender #e6e6fa 230 230 250
lavenderblush #fff0f5 255 240 245
lawngreen #7cfc00 124 252 0
lemonchiffon #fffacd 255 250 205
lightblue #add8e6 173 216 230
lightcoral #f08080 240 128 128
lightcyan #e0ffff 224 255 255
lightgoldenrodyellow #fafad2 250 250 210
lightgray #d3d3d3 211 211 211
lightgreen #90ee90 144 238 144
lightgrey #d3d3d3 211 211 211
lightpink #ffb6c1 255 182 193
lightsalmon #ffa07a 255 160 122
lightseagreen #20b2aa 32 178 170
lightskyblue #87cefa 135 206 250
lightslategray #778899 119 136 153
lightslategrey #778899 119 136 153
lightsteelblue #b0c4de 176 196 222
lightyellow #ffffe0 255 255 224
lime #00ff00 0 255 0
limegreen #32cd32 50 205 50
linen #faf0e6 250 240 230
magenta #ff00ff 255 0 255
maroon #800000 128 0 0
mediumaquamarine #66cdaa 102 205 170
mediumblue #0000cd 0 0 205
mediumorchid #ba55d3 186 85 211
mediumpurple #9370db 147 112 219
mediumseagreen #3cb371 60 179 113
mediumslateblue #7b68ee 123 104 238
mediumspringgreen #00fa9a 0 250 154
mediumturquoise #48d1cc 72 209 204
mediumvioletred #c71585 199 21 133
midnightblue #191970 25 25 112
mintcream #f5fffa 245 255 250
mistyrose #ffe4e1 255 228 225
moccasin #ffe4b5 255 228 181
navajowhite #ffdead 255 222 173
navy #000080 0 0 128
oldlace #fdf5e6 253 245 230
olive #808000 128 128 0
olivedrab #6b8e23 107 142 35
orange #ffa500 255 165 0
orangered #ff4500 255 69 0
orchid #da70d6 218 112 214
palegoldenrod #eee8aa 238 232 170
palegreen #98fb98 152 251 152
paleturquoise #afeeee 175 238 238
palevioletred #db7093 219 112 147
papayawhip #ffefd5 255 239 213
peachpuff #ffdab9 255 218 185
peru #cd853f 205 133 63
pink #ffc0cb 255 192 203
plum #dda0dd 221 160 221
powderblue #b0e0e6 176 224 230
purple #800080 128 0 128
rebeccapurple #663399 102 51 153
red #ff0000 255 0 0
rosybrown #bc8f8f 188 143 143
royalblue #4169e1 65 105 225
saddlebrown #8b4513 139 69 19
salmon #fa8072 250 128 114
sandybrown #f4a460 244 164 96
seagreen #2e8b57 46 139 87
seashell #fff5ee 255 245 238
sienna #a0522d 160 82 45
silver #c0c0c0 192 192 192
skyblue #87ceeb 135 206 235
slateblue #6a5acd 106 90 205
slategray #708090 112 128 144
slategrey #708090 112 128 144
snow #fffafa 255 250 250
springgreen #00ff7f 0 255 127
steelblue #4682b4 70 130 180
tan #d2b48c 210 180 140
teal #008080 0 128 128
thistle #d8bfd8 216 191 216
tomato #ff6347 255 99 71
turquoise #40e0d0 64 224 208
violet #ee82ee 238 130 238
wheat #f5deb3 245 222 179
white #ffffff 255 255 255
whitesmoke #f5f5f5 245 245 245
yellow #ffff00 255 255 0
yellowgreen #9acd32 154 205 50

Note: this list of colors and their definitions is a superset of the list of named colors defined by SVG 1.1.

For historical reasons, this is also referred to as the X11 color set.

Note: The history of the X11 color system is interesting, and was excellently summarized by Alex Sexton in his talk “Peachpuffs and Lemonchiffons”.

Tests

6.2. System Colors

In general, the <system-color> keywords reflect default color choices made by the user, the browser, or the OS. They are typically used in the browser default stylesheet, for this reason.

To maintain legibility, the <system-color> keywords also respond to light mode or dark mode changes.

However, in forced colors mode, most colors on the page are forced into a restricted, user-chosen palette. The <system-color> keywords expose these user-chosen colors so that the rest of the page can integrate with this restricted palette.

When the forced-colors media feature is active, authors should use the <system-color> keywords as color values in properties other than those listed in CSS Color Adjust § 3.1 Properties Affected by Forced Colors Mode, to ensure legibility and consistency across the page and avoid an uncoordinated mishmash of user-forced and page-chosen colors.

Tests

When the values of <system-color> keywords come from the browser, (as opposed to being OS defaults or user choices) the browser should ensure that matching foreground/background pairs have a minimum of WCAG AA contrast. However, user preferences (for higher or lower contrast), whether set as a browser preference, a user stylesheet, or by altering the OS defaults, must take precedence over this requirement.

Authors may also use these keywords at any time, but should be careful to use the colors in matching background-foreground pairs to ensure appropriate contrast, as any particular contrast relationship across non-matching pairs (e.g. Canvas and ButtonText) is not guaranteed.

The <system-color> keywords are defined as follows:

Canvas
 Background of application content or documents.
CanvasText
 Text in application content or documents.
LinkText
 Text in non-active, non-visited links. For light backgrounds, traditionally blue.
VisitedText
 Text in visited links. For light backgrounds, traditionally purple.
ActiveText
 Text in active links. For light backgrounds, traditionally red.
ButtonFace
 The face background color for push buttons.
ButtonText
 Text on push buttons.
ButtonBorder
 The base border color for push buttons.
Field
 Background of input fields.
FieldText
 Text in input fields.
Highlight
 Background of selected text, for example from ::selection.
HighlightText
 Text of selected text.
SelectedItem
 Background of selected items, for example a selected checkbox.
SelectedItemText
 Text of selected items.
Mark
 Background of text that has been specially marked (such as by the HTML mark element).
MarkText
 Text that has been specially marked (such as by the HTML mark element).
GrayText
 Disabled text. (Often, but not necessarily, gray.)
Tests

Note: As with all other keywords, these names are ASCII case-insensitive. They are shown here with mixed capitalization for legibility.

For systems that do not have a particular system UI concept, the specified value should be mapped to the most closely related system color value that exists. The following system color pairings are expected to form legible background-foreground colors:

Additionally, GrayText is expected to be readable, though possibly at a lower contrast rating, over any of the backgrounds.

For example, the system color combinations in the browser you are currently using:

Canvas with CanvasText CanvasText

Canvas with LinkText LinkText

Canvas with VisitedText VisitedText

Canvas with ActiveText ActiveText

Canvas with GrayText GrayText

Canvas with ButtonBorder and adjacent Canvas CanvasTextAdjacent

ButtonFace with ButtonText ButtonText

ButtonFace with ButtonText and ButtonBorder ButtonText

ButtonFace with GrayText GrayText

Field with FieldText FieldText

Field with GrayText GrayText

Mark with MarkText MarkText

Mark with GrayText GrayText

Highlight with HighlightText HighlightText

Highlight with GrayText GrayText

SelectedItem with SelectedItemText SelectedItemText

Earlier versions of CSS defined additional <system-color>s, which have since been deprecated. These are documented in Appendix A: Deprecated CSS System Colors.

Note: The <system-color>s incur some privacy and security risk, as detailed in § 18 Privacy Considerations and § 17 Security Considerations.

6.3. The transparent keyword

The keyword transparent specifies a transparent black. It is a type of <named-color>.

Tests

6.4. The currentcolor keyword

The keyword currentcolor represents value of the color property on the same element. Unlike <named-color>s, it is not restricted to sRGB; the value can be any <color>. Its used values is determined by resolving color values.

Tests
Here’s a simple example showing how to use the currentcolor keyword:
.foo {
  color:  red;
  background-color:  currentcolor;
}

This is equivalent to writing:

.foo {
  color:  red;
  background-color:  red;
}
For example, the text-emphasis-color property [CSS3-TEXT-DECOR], whose initial value is currentcolor, by default matches the text color even as the color property changes across elements.
<p><em>Some <strong>really</strong> emphasized text.</em>
<style>
p { color: black; }
em { text-emphasis: dot; }
strong { color: red; }
</style>

rendered emphasized text with the word 'really' in red with red emphasis dots

In the above example, the emphasis marks are black over the text "Some" and "emphasized text", but red over the text "really".

Note: Multi-word keywords in CSS usually separate their component words with hyphens. currentcolor doesn’t, because (deep breath) it was originally introduced in SVG as a property value, "current-color" with the usual CSS spelling. It (along with all other properties and their values) then became presentation attributes and attribute values, as well as properties, to make generation with XSLT easier. Then all of the presentation attributes were changed from hyphenated to camelCase, because the DOM had an issue with hyphen meaning "minus". But then, they didn’t follow CSS conventions anymore so all the properties and property values that were already part of CSS were changed back to hyphenated! currentcolor was not a part of CSS at that time, so remained camelCased. Only later did CSS pick it up, at which point the capitalization stopped mattering, as CSS keywords are ASCII case-insensitive.

7. HSL Colors: hsl() and hsla() functions

The RGB system for specifying colors, while convenient for machines and graphic libraries, is often regarded as very difficult for humans to gain an intuitive grasp on. It’s not easy to tell, for example, how to alter an RGB color to produce a lighter variant of the same hue.

There are several other color schemes possible. One such is the HSL [HSL] color scheme, which is much more intuitive to use, but still maps easily back to RGB colors.

HSL colors are specified as a triplet of hue, saturation, and lightness. The syntax of the hsl() function is:

hsl() = hsl( [<hue> | none] [<percentage> | none] [<percentage> | none] [ / [<alpha-value> | none] ]? )
Tests

The first argument specifies the hue angle.

In HSL (and HWB) the angle 0deg represents sRGB primary red (as does 360deg, 720deg, etc.), and the rest of the hues are spread around the circle, so 120deg represents sRGB primary green, 240deg represents sRGB primary blue, etc.

The next two arguments are the saturation and lightness, respectively. For saturation, 100% is a fully-saturated, bright color, and 0% is a fully-unsaturated gray. For lightness, 50% represents the "normal" color, while 100% is white and 0% is black. If the saturation or lightness are less than 0% or greater than 100%, they are clamped to those values at computed value time, before being converted to an sRGB color.

The final argument specifies the alpha channel of the color. It’s interpreted identically to the fourth argument of the rgb() function. If omitted, it defaults to 100%.

HSL colors resolve to sRGB.

If the saturation of an HSL color is 0%, then the hue component is powerless. If the lightness of an HSL color is 0% or 100%, both the saturation and hue components are powerless.

For example, an ordinary red, the same color you would see from the keyword  red or the hex notation  #f00, is represented in HSL as  hsl(0deg 100% 50%).

An advantage of HSL over RGB is that it is more intuitive: people can guess at the colors they want, and then tweak.

For example, the following colors can all be generated off of the basic "green" hue, just by varying the other two arguments:
hsl(120deg 100% 50%) lime green
hsl(120deg 100% 25%) dark green
hsl(120deg 100% 75%) light green
hsl(120deg 75% 85%)  pastel green

An advantage of HSL over LCH is that, regardless of manipulation, the result always lies inside the sRGB gamut. A disadvantage of HSL over LCH is that hue manipulation changes the visual lightness, and that hues are not evenly spaced apart.

It is thus easier in HSL to create sets of matching colors (by keeping the hue the same and varying the saturation and lightness), compared to manipulating the sRGB component values; however, because the lightness is simply the mean of the gamma-corrected red, green and blue components it does not correspond to the visual perception of lightness across hues.

For example,  blue is represented in HSL as  hsl(240deg 100% 50%) while  yellow is  hsl(60deg 100% 50%). Both have an HSL Lightness of 50%, but clearly the yellow looks much lighter than the blue.

In LCH, sRGB blue is  lch(29.6% 131.2 301.3) while sRGB yellow is  lch(97.6% 94.7 99.6). The LCH Lightnesses of 29.6% and 97.6% clearly reflect the visual lightnesses of the two colors.

The hue angle in HSL is not perceptually uniform; colors appear bunched up in some areas and widely spaced in others.

For example, the pair of hues  hsl(220deg 100% 50%) and  hsl(250deg 100% 50%) have an HSL hue difference of 250-220 = 30deg and look fairly similar, while another pair of colors  hsl(50deg 100% 50%) and  hsl(80deg 100% 50%), which also have a hue difference of 80-50 = 30deg, look very different.

In LCH, the same pair of colors  lch(42.1% 97.4 290.6) and  lch(30.8% 129.7 302.1) have a hue difference of 302.1-290.6 = 11.5deg while the second pair  lch(86.8% 86.2 87.3) and  lch(92.0% 98.8 119.1) have a hue difference of 119.1-87.3 = 31.8deg, correctly reflecting the visual separation of hues.

For legacy reasons, hsl() also supports a legacy color syntax that separates all of its arguments with commas:

hsl() = hsl( <hue>, <percentage>, <percentage>, <alpha-value>? )

Also, an hsla() legacy color syntax also exists, with an identical grammar and behavior to hsl().

Tests

7.1. Converting HSL colors to sRGB colors

Converting an HSL color to sRGB is straightforward mathematically. Here’s a sample implementation of the conversion algorithm in JavaScript. It returns an array of three numbers representing the red, green, and blue channels of the colors, normalized to the range [0, 1].

function hslToRgb (hue, sat, light) {
    hue = hue % 360;

    if (hue < 0) {
        hue += 360;
    }

    sat /= 100;
    light /= 100;

    function f(n) {
        let k = (n + hue/30) % 12;
        let a = sat * Math.min(light, 1 - light);
        return light - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
    }

    return [f(0), f(8), f(4)];
}

7.2. Converting sRGB colors to HSL colors

Conversion in the reverse direction proceeds similarly.

function rgbToHsl (red, green, blue) {
    let max = Math.max(red, green, blue);
    let min = Math.min(red, green, blue);
    let [hue, sat, light] = [NaN, 0, (min + max)/2];
    let d = max - min;

    if (d !== 0) {
        sat = (light === 0 || light === 1)
            ? 0
            : (max - light) / Math.min(light, 1 - light);

        switch (max) {
            case red:   hue = (green - blue) / d + (green < blue ? 6 : 0); break;
            case green: hue = (blue - red) / d + 2; break;
            case blue:  hue = (red - green) / d + 4;
        }

        hue = hue * 60;
    }

    return [hue, sat * 100, light * 100];
}

7.3. Examples of HSL colors

The tables below illustrate a wide range of possible HSL colors. Each table represents one hue, selected at 30° intervals, to illustrate the common "core" hues: red, yellow, green, cyan, blue, magenta, and the six intermediary colors between these.

In each table, the X axis represents the saturation while the Y axis represents the lightness.

0° Reds
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
30° Reds-Yellows (=Oranges)
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
60° Yellows
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
90° Yellow-Greens
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
120° Greens
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
150° Green-Cyans
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
180° Cyans
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
210° Cyan-Blues
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
240° blues
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
270° Blue-Magentas
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
300° Magentas
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%
330° Magenta-Reds
100% 80% 60% 40% 20% 0%
100%
90%
80%
70%
60%
50%
40%
30%
20%
10%
0%

8. HWB Colors: hwb() function

HWB (short for Hue-Whiteness-Blackness) [HWB] is another method of specifying sRGB colors, similar to HSL', but often even easier for humans to work with. It describes colors with a starting hue, then a degree of whiteness and blackness to mix into that base hue.

Many color-pickers are based on the HWB color system, due to its intuitiveness.

HWB colors resolve to sRGB.

This is a screenshot of Chrome’s color picker, shown when a user activates an <input type="color">. The outer wheel is used to select the hue, then the relative amounts of white and black are selected by clicking on the inner triangle.

The syntax of the hwb() function is:

hwb() = hwb( [<hue> | none] [<percentage> | none] [<percentage> | none] [ / [<alpha-value> | none] ]? )

The first argument specifies the hue, and is interpreted identically to hsl().

The second argument specifies the amount of white to mix in, as a percentage from 0% (no whiteness) to 100% (full whiteness). Similarly, the third argument specifies the amount of black to mix in, also from 0% (no blackness) to 100% (full blackness). Values outside of these ranges are not invalid, but are clamped to the ranges defined here at computed-value time. If the sum of these two arguments is greater than 100%, then at computed-value time they are further normalized to add up to 100%, with the same relative ratio.

The fourth argument specifies the alpha channel of the color. It’s interpreted identically to the fourth argument of the rgb() function. If omitted, it defaults to 100%.

The resulting color can be thought of conceptually as a mixture of paint in the chosen hue, white paint, and black paint, with the relative amounts of each determined by the percentages. If white+black is equal to 100% (after normalization), it defines an achromatic color, i.e. some shade of gray, without any hint of the chosen hue. In this case, the hue component is powerless.

There is no Web compatibility issue with hwb, which is new in this level of the specification, and so hwb() does not support a legacy color syntax that separates all of its arguments with commas. Using commas inside hwb() is an error.

Tests

8.1. Converting HWB colors to sRGB colors

Converting an HWB color to sRGB is straightforward, and related to how one converts HSL to RGB. The following Javascript implementation of the algorithm first normalizes the white and black components, so their sum is no larger than 100%.

function hwbToRgb(hue, white, black) {
    white /= 100;
    black /= 100;
    if (white + black >= 1) {
        let gray = white / (white + black);
    return [gray, gray, gray];
    }
    let rgb = hslToRgb(hue, 100, 50);
    for (let i = 0; i < 3; i++) {
        rgb[i] *= (1 - white - black);
        rgb[i] += white;
    }
    return rgb;
}

8.2. Converting sRGB colors to HWB colors

Conversion in the reverse direction proceeds similarly.

function rgbToHwb(red, green, blue) {
    var hsl = rgbToHsl(red, green, blue);
    var white = Math.min(red, green, blue);
    var black = 1 - Math.max(red, green, blue);
    return([hsl[0], white*100, black*100]);
}

8.3. Examples of HWB Colors

0° Reds
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
30° Red-Yellows (Oranges)
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
60° Yellows
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
90° Yellow-Greens
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
120° Greens
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
150° Green-Cyans
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
180° Cyans
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
210° Cyan-Blues
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
240° Blues
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
270° Blue-Magentas
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
300° Magentas
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%
330° Magenta-Reds
W\B 0% 20% 40% 60% 80% 100%
0%
20%
40%
60%
80%
100%

9. Device-independent Colors: CIE Lab and LCH, OKLab and OKLCH

9.1. CIE Lab and LCH

Physical measurements of a color are typically expressed in the CIE L*a*b* [CIELAB] color space, created in 1976 by the CIE and commonly referred to simply as Lab. Color conversions from one device to another may also use Lab as an intermediate step. Derived from human vision experiments, Lab represents the entire range of color that humans can see.

Lab is a rectangular coordinate system with a central Lightness axis. This value is usually written as a unitless number; for compatibility with the rest of CSS, it is written as a percentage. 100% means an L value of 100, not 1.0. L=0% is deep black (no light at all) while L=100% is a diffuse white

Values of L greater than 100 would correspond to specular highlights, but their precise color is undefined in this specification. Usefully, L=50% is mid gray, by design, and equal increments in L are evenly spaced visually: the Lab color space is intended to be perceptually uniform.

This figure shows the Lightness axis of the CIE Lab color space. Twenty-one neutral swatches are shown (L=0%, L=5%, to L=100%). The steps are equally spaced, visually.

The a and b axes convey hue; positive values along the a axis are a purplish red while negative values are the complementary color, a green. Similarly, positive values along the b axis are yellow and negative are the complementary blue/violet. Desaturated colors have small values of a and b and are close to the L axis; saturated colors lie far from the L axis.

The illuminant is D50 white, a standardized daylight spectrum with a color temperature of 5000K, as reflected by a perfect diffuse reflector; it approximates the color of sunlight on a sunny day. D50 is also the whitepoint used for the profile connection space in ICC color interconversion, the whitepoint used in image editors which offer Lab editing, and the value used by physical measurement devices such as spectrophotometers and spectroradiometers, when they report measured colors in Lab.

Conversion from colors specified using other white points is called a chromatic adaptation transform, which models the changes in the human visual system as we adapt to a new lighting condition. The Bradford algorithm [Bradford-CAT] is the industry standard chromatic adaptation transform, and is easy to calculate as it is a simple matrix multiplication.

CIE LCH has the same L axis as Lab, but uses polar coordinates C (chroma) and H (hue), making it a polar, cylindrical coordinate system. C is the geometric distance from the L axis and H is the angle from the positive a axis, towards the positive b axis.

This figure shows the L=50 plane of the CIE Lab color space. 20 degree increments in CIE LCH are displayed as circles at three levels of Chroma: 20, 40 and 60. All the 20 Chroma colors fit inside sRGB gamut, some of 40 and 60 Chroma are outside. These out of gamut colors are visualized as grey, with a red warning outer stroke.

Note: The Lightness axis in Lab and LCH is not to be confused with the L axis in HSL. For example, in HSL, the sRGB colors blue (#00F) and yellow (#FF0) have the same value of L (50%) even though visually, blue is much darker. This is much clearer in Lab: sRGB blue is lab(29.567% 68.298 -112.0294) while sRGB yellow is lab(97.607% -15.753 93.388). In Lab and LCH, if two colors have the same measured L value, they have identical visual lightness. HSL and related polar RGB models were developed in an attempt to give similar usability benefits for RGB that LCH gave to Lab, but are significantly less accurate.

Although the use of CIE Lab and LCH is widespread, it is known to have some problems. In particular:

Hue linearity
In the blue region (LCH Hue between 270° and 330°), visual hue departs from what LCH predicts. Plotting a set of blues of the same hue and differing Chroma, which should lie on a straight line from the neutral axis, instead form a curve. Put another way, as a saturated blue has it’s Chroma progressively reduced, it becomes noticeably purple.
Hue uniformity
While hues in LCH are in general evenly spaced, (and far better than HSL or HWB), uniformity is not perfect.
Over-prediction of high Chroma differences
For high Chroma colors, changes in Chroma are less noticeable than for more neutral colors.

These deficiencies affect, for example, creation of evenly spaced gradients, gamut mapping from one color space to a smaller one, and computation of the visual difference between two colors.

To compensate for this, formulae to predict the visual difference betwen two colors (delta E) have been made more accurate over time (but also, much more complex to compute). The current industry standard formula, delta E 2000, works well to mitigate some of the Lab and LCH problems. A sample implementation is given in § 16.1 ΔE2000.

This does not help with hue curvature, however.

9.2. OKLab and OKLCH

Recently, OKLab, an improved Lab-like space has been developed [OKLab]. The corresponding polar form is called OKLCH. It was produced by numerical optimisation of a large dataset of visually similar colors, and has improved hue linearity, hue uniformity, and chroma uniformity compared to CIE LCH.

Like CIE Lab, there is a central lightness axis which is usually written as a unitless number in the range [0,1]; for compatibility with the rest of CSS, it is written as a percentage. 100% means an L value of 1.0. L=0% is deep black (no light at all) while L=100% is a diffuse white.

Note: Unlike CIE Lab, which assumes adaptation to the diffuse white, OKLab assumes adaptation to the color being defined, which is intended to make it scale invariant.

As with CIE Lab, the a and b axes convey hue; positive values along the a axis are a purplish red while negative values are the complementary color, a green. Similarly, positive values along the b axis are yellow and negative are the complementary blue/violet.

The illuminant is D65, the same white point as most RGB color spaces.

OKLCH has the same L axis as OKLab, but uses polar coordinates C (chroma) and H (hue).

Note: Unlike CIE LCH, where Chroma can reach values of 200 or more, OKLCH Chroma ranges to 0.5 or so. The hue angles between CIE LCH and OKLCH are broadly similar, but not identical.

diagram showing purpling in CIE LCH
A constant CIE LCH hue slice, showing the sRGB gamut around primary blue. A noticeable purpling is immediately evident.
diagram showing hue constancy in OKLCH
A constant OKLCH hue slice, showing the sRGB gamut around primary blue. The visual hue remains constant.

Because OKLab is more perceptually uniform, the color difference is a straightforward distance in 3D space (root sum of squares). Although trivial, a sample implementation is give in § 16.2 ΔEOK.

9.3. Specifying Lab and LCH: the lab() and lch() functional notations

CSS allows colors to be directly expressed in Lab and LCH.

lab() = lab( [<percentage> | <number> | none]
      [ <percentage> | <number> | none]
      [ <percentage> | <number> | none]
      [ / [<alpha-value> | none] ]? )
Percentages Allowed for L, a and b
Percent reference range  for L: 0% = 0.0, 100% = 100.0
for a and b: -100% = -125, 100% = 125
Tests

The first argument specifies the CIE Lightness. This is typically a number between 0% (representing black) and 100% (representing white), However, CIE Lightness can exceed this range on some systems, with extra-bright whites using a lightness up to 400. Values less than 0% must be clamped to 0% at computed-value time; values greater than 100% are permitted (for forwards compatibility with High Dynamic Range (HDR), and must not be clamped.

The second and third arguments are the distances along the "a" and "b" axes in the Lab color space, as described in the previous section. These values are signed (allow both positive and negative values) and theoretically unbounded (but in practice do not exceed ±160).

There is an optional fourth alpha value, separated by a slash, and interpreted identically to the <alpha-value> in rgb().

If the lightness of a Lab color is 0%, both the a and b components are powerless.

Unsure about when/if a and b become powerless for Lab whites.

 lab(29.2345% 39.3825 20.0664);
 lab(52.2345% 40.1645 59.9971);
 lab(60.2345 -5.3654 58.956);
 lab(62.2345% -34.9638 47.7721);
 lab(67.5345% -8.6911 -41.6019);
 lab(29.69% 44.888% -29.04%)
lch() = lch( [<percentage> | <number> | none]
      [ <percentage> | <number> | none]
      [ <hue> | none]
      [ / [<alpha-value> | none] ]? )
Percentages Allowed for L and C
Percent reference range  for L: 0% = 0.0, 100% = 100.0
for C: 0% = 0, 100% = 150
Tests

The first argument specifies the CIE Lightness, interpreted identically to the Lightness argument of lab().

The second argument is the chroma (roughly representing the "amount of color"). Its minimum useful value is 0, while its maximum is theoretically unbounded (but in practice does not exceed 230). If the provided value is negative, it is clamped to 0 at computed-value time.

The third argument is the hue angle. It’s interpreted similarly to the <hue> argument of hsl(), but doesn’t map hues to angles in the same way because they are evenly spaced perceptually. Instead, 0deg points along the positive "a" axis (toward purplish red), (as does 360deg, 720deg, etc.); 90deg points along the positive "b" axis (toward mustard yellow), 180deg points along the negative "a" axis (toward greenish cyan), and 270deg points along the negative "b" axis (toward sky blue).

There is an optional fourth alpha value, separated by a slash, and interpreted identically to the <alpha-value> in rgb().

If the chroma of an LCH color is 0%, the hue component is powerless. If the lightness of an LCH color is 0%, both the hue and chroma components are powerless.

Unsure about when/if hue and chroma become powerless for LCH whites.

 lch(29.2345% 44.2 27);
 lch(52.2345% 72.2 56.2);
 lch(60.2345 59.2 95.2);
 lch(62.2345% 59.2 126.2);
 lch(67.5345% 42.5 258.2);
 lch(29.69% 45.553% 327.1)

There is no Web compatibility issue with lab or lch', which are new in this level of the specification, and so lab() and lch() do not support a legacy color syntax that separates all of their arguments with commas. Using commas inside these functions is an error.

9.4. Specifying OKLab and OKLCH: the oklab() and oklch() functional notations

CSS allows colors to be directly expressed in OKLab and OKLCH.

oklab() = oklab( [ <percentage> | <number> | none]
    [ <percentage> | <number> | none]
    [ <percentage> | <number> | none]
    [ / [<alpha-value> | none] ]? )
Percentages Allowed for L, a and b
Percent reference range  for L: 0% = 0.0, 100% = 1.0
for a and b: -100% = -0.4, 100% = 0.4
Tests

The first argument specifies the OKLab Lightness. This is typically a number between 0% (representing black) and 100% (representing white).

Values less than 0% must be clamped to 0% at computed-value time; values greater than 100% are permitted for forwards compatibility, and must not be clamped.

The second and third arguments are the distances along the "a" and "b" axes in the OKLab color space, as described in the previous section. These values are signed (allow both positive and negative values) and theoretically unbounded (but in practice do not exceed ±0.5).

There is an optional fourth alpha value, separated by a slash, and interpreted identically to the <alpha-value> in rgb().

If the lightness of an OKLab color is 0%, both the a and b components are powerless.

 oklab(40.101% 0.1147 0.0453);
 oklab(59.686% 0.1009 0.1192);
 oklab(0.65125 -0.0320 0.1274);
 oklab(66.016% -0.1084 0.1114);
 oklab(72.322% -0.0465 -0.1150);
 oklab(42.1% 41% -25%)
oklch() = oklch( [ <percentage> | <number> | none]
      [ <percentage> | <number> | none]
      [ <hue> | none]
      [ / [<alpha-value> | none] ]? )
Percentages Allowed for L and C
Percent reference range  for L: 0% = 0.0, 100% = 1.0
for C: 0% = 0.0 100% = 0.4
Tests

The first argument specifies the OKLCH Lightness, interpreted identically to the Lightness argument of oklab().

The second argument is the chroma. Its minimum useful value is 0, while its maximum is theoretically unbounded (but in practice does not exceed 0.5). If the provided value is negative, it is clamped to 0 at computed-value time.

The third argument is the hue angle. It’s interpreted similarly to the <hue> arguments of hsl() and lch(), but doesn’t map hues to angles in the same way. 0deg points along the positive "a" axis (toward purplish red), (as does 360deg, 720deg, etc.); 90deg points along the positive "b" axis (toward mustard yellow), 180deg points along the negative "a" axis (toward greenish cyan), and 270deg points along the negative "b" axis (toward sky blue).

There is an optional fourth alpha value, separated by a slash, and interpreted identically to the <alpha-value> in rgb().

If the chroma of an OKLCH color is 0%, the hue component is powerless. If the lightness of an OKLCH color is 0%, both the hue and chroma components are powerless.

 oklch(40.101% 0.12332 21.555);
 oklch(59.686% 0.15619 49.7694);
 oklch(0.65125 0.13138 104.097);
 oklch(0.66016 0.15546 134.231);
 oklch(72.322% 0.12403 247.996);
 oklch(42.1% 48.25% 328.4)

There is no Web compatibility issue with oklab or oklch', which are new in this level of the specification, and so oklab() and oklch() do not support a legacy color syntax that separates all of their arguments with commas. Using commas inside these functions is an error.

9.5. Converting Lab or OKLab colors to LCH or OKLCH colors

Conversion to the polar form is trivial:

  1. H = atan2(b, a)
  2. C = sqrt(a^2 + b^2)
  3. L is the same

For extremely small values of a and b (near-zero Chroma), although the visual color does not change from being on the neutral axis, small changes to the values can result in the reported hue angle swinging about wildly and being essentially random. In CSS, this means the hue is powerless, and treated as missing when converted into LCH or OKLCH; in non-CSS contexts this might be reflected as a missing value, such as NaN.

9.6. Converting LCH or OKLCH colors to Lab or OKLab colors

Conversion to the rectangular form is trivial:

  1. If H is missing, a = b = 0
  2. Otherwise,
    1. a = C cos(H)
    2. b = C sin(H)
  3. L is the same

10. Predefined Color Spaces

CSS provides several predefined color spaces including display-p3 [Display-P3], which is a wide gamut space typical of current wide-gamut monitors, prophoto-rgb, widely used by photographers and rec2020 [Rec.2020], which is a broadcast industry standard, ultra-wide gamut space capable of representing almost all visible real-world colors.

10.1. Specifying predefined colors: the color() function

The color() function allows a color to be specified in a particular, specified color space (rather than the implicit sRGB color space that most of the other color functions operate in). Its syntax is:

color() = color( <colorspace-params> [ / <alpha-value> ]? )
<colorspace-params> = [ <predefined-rgb-params> | <xyz-params>]
<predefined-rgb-params> = <predefined-rgb> [ <number> | <percentage> | none ]{3}
<predefined-rgb> = srgb | srgb-linear | display-p3 | a98-rgb | prophoto-rgb | rec2020
<xyz-params> = <xyz-space> [ <number> | none ]{3}
<xyz-space> = xyz | xyz-d50 | xyz-d65
Tests

The color function takes parameters specifying a color, in an explicitly listed color space.

It represents either an invalid color, as described below, or a valid color.

Any color which is not an invalid color is a valid color.

A color may be a valid color but still be outside the range of colors that can be produced by an output device (a screen, projector, or printer). It is said to be out of gamut for that color space.

An out of gamut color has component values less than 0 or 0%, or greater than 1 or 100%. These are not invalid; instead, for display, they are css gamut mapped using a relative colorimetric intent which brings the values within the range 0/0% to 1/100% at computed-value time.

Each valid color is either in-gamut for the output device (screen, or printer), or it is out of gamut.

The parameters have the following form:

Tests

There is no Web compatibility issue with color(), which is new in this level of the specification, and so color() does not support a legacy color syntax that separates all of its arguments with commas. Using commas inside this function is an error.

A color which is either an invalid color or an out of gamut color can’t be displayed.

If the specified color can be displayed, (that is, it isn’t an invalid color and isn’t out of gamut) then this is the used value of the color() function.

If the specified color is a valid color but can’t be displayed], the used value is derived from the specified color, css gamut mapped for display.

If the color is an invalid color, the used value is opaque black.

This very intense lime color is in-gamut for rec.2020:
color(rec2020 0.42053 0.979780 0.00579);

in LCH, that color is

lch(86.6146% 160.0000 136.0088);

in display-p3, that color is

color(display-p3 -0.6112 1.0079 -0.2192);

and is out of gamut for display-p3 (red and blue are negative, green is greater than 1). If you have a display-p3 screen, that color is:

The color used for display will be a less intense color produced automatically by gamut mapping.
This example has a typo! An intense green is provided in profoto-rgb space (which doesn’t exist). This makes it invalid, so the used value is opaque black
color(profoto-rgb 0.4835 0.9167 0.2188)

10.2. Predefined sRGB space

The sRGB predefined color space defined below is the same as is used for legacy sRGB colors, such as rgb().

srgb
The srgb [SRGB] color space accepts three numeric parameters, representing the red, green, and blue channels of the color. In-gamut colors have all three components in the range [0, 1]. The whitepoint is D65.

[SRGB] specifies two viewing conditions, encoding and typical. The [ICC] recommends using the encoding conditions for color conversion and for optimal viewing, which are the values in the table below.

sRGB is the default color space for CSS, used for all the legacy color functions.

It has the following characteristics:

x y
Red chromaticity 0.640 0.330
Green chromaticity 0.300 0.600
Blue chromaticity 0.150 0.060
White chromaticity D65
Transfer function see below
White luminance 80.0 cd/m2
Black luminance 0.20 cd/m2
Image state display-referred
Percentages Allowed for R, G and B
Percent reference range  for R,G,B: 0% = 0.0, 100% = 1.0
let sign = c < 0? -1 : 1;
let abs = Math.abs(c);

if (abs < 0.04045) {
  cl = c / 12.92;
}
else {
  cl = sign * (Math.pow((abs + 0.055) / 1.055, 2.4));
}

c is the gamma-encoded red, green or blue component. cl is the corresponding linear-light component.

diagram of sRGB primaries and secondaries in LCH
Visualization of the sRGB color space in LCH. The primaries and secondaries are shown.
Tests

10.3. Predefined sRGB-linear space

The sRGB-linear predefined color space is the same as srgb except that the transfer function is linear-light (there is no gamma-encoding).

srgb-linear
The srgb-linear [SRGB] color space accepts three numeric parameters, representing the red, green, and blue channels of the color. In-gamut colors have all three components in the range [0, 1]. The whitepoint is D65.

It has the following characteristics:

x y
Red chromaticity 0.640 0.330
Green chromaticity 0.300 0.600
Blue chromaticity 0.150 0.060
White chromaticity D65
Transfer function unity, see below
White luminance 80.0 cd/m2
Black luminance 0.20 cd/m2
Image state display-referred
Percentages Allowed for R, G and B
Percent reference range  for R,G,B: 0% = 0.0, 100% = 1.0
cl = c;

c is the red, green or blue component. cl is the corresponding linear-light component, which is identical.

Tests

To avoid banding artifacts, a higher precision is required for srgb-linear than for srgb.

For example, these are the same color
 color(srgb 0.691 0.139 0.259)
 color(srgb-linear 0.435 0.017 0.055)

10.4. Predefined display-p3 space

display-p3
The display-p3 [Display-P3] color space accepts three numeric parameters, representing the red, green, and blue channels of the color. In-gamut colors have all three components in the range [0, 1]. It uses the same primary chromaticities as [DCI-P3], but with a D65 whitepoint, and the same transfer curve as sRGB.

Modern displays, TVs, laptop screens and phone screens are able to display all, or nearly all, of the display-p3 gamut.

It has the following characteristics:

x y
Red chromaticity 0.680 0.320
Green chromaticity 0.265 0.690
Blue chromaticity 0.150 0.060
White chromaticity D65
Transfer function same as srgb
White luminance 80.0 cd/m2
Black luminance 0.80 cd/m2
Image state display-referred
Percentages Allowed for R, G and B
Percent reference range  for R,G,B: 0% = 0.0, 100% = 1.0
diagram of P3 primaries and secondaries in LCH
Visualization of the P3 color space in LCH. The primaries and secondaries are shown (but in sRGB, not in the correct colors). For comparison, the sRGB primaries and secondaries are also shown, as dashed circles. P3 primaries have higher Chroma.
Tests

10.5. Predefined a98-rgb space

a98-rgb
The a98-rgb color space accepts three numeric parameters, representing the red, green, and blue channels of the color. In-gamut colors have all three components in the range [0, 1]. The transfer curve is a gamma function, close to but not exactly 1/2.2.

It has the following characteristics:

x y
Red chromaticity 0.6400 0.3300
Green chromaticity 0.2100 0.7100
Blue chromaticity 0.1500 0.0600
White chromaticity D65
Transfer function 256/563
White luminance 160.0 cd/m2
Black luminance 0.5557 cd/m2
Image state display-referred
Percentages Allowed for R, G and B
Percent reference range  for R,G,B: 0% = 0.0, 100% = 1.0
diagram of a98 primaries and secondaries in LCH
Visualization of the a98 color space in LCH. The primaries and secondaries are shown (but in sRGB, not in the correct colors). For comparison, the sRGB primaries and secondaries are also shown, as dashed circles. a98 primaries have higher Chroma, especially the yellow, green and cyan.
Tests

10.6. Predefined prophoto-rgb space

prophoto-rgb
The prophoto-rgb color space accepts three numeric parameters, representing the red, green, and blue channels of the color. In-gamut colors have all three components in the range [0, 1]. The transfer curve is a gamma function with a value of 1/1.8, and a small linear portion near black. The white point is D50, the same as is used by CIE Lab. Thus, conversion to CIE Lab does not require the chromatic adaptation step.

The ProPhoto RGB space uses hyper-saturated, non physically realizable primaries. These were chosen to allow a wide color gamut and in particular, to minimize hue shifts under tonal manipulation. It is often used in digital photography as a wide gamut color space for the archival version of photographic images. The prophoto-rgb color space allows CSS to specify colors that will match colors in such images having the same RGB values.

The ProPhoto RGB space was originally developed by Kodak and is described in [Wolfe]. It was standardized by ISO as [ROMM],[ROMM-RGB].

The white luminance is given as a range, and the viewing flare (and thus, the black luminance) is 0.5% to 1.0% of this.

It has the following characteristics:

x y
Red chromaticity 0.734699 0.265301
Green chromaticity 0.159597 0.840403
Blue chromaticity 0.036598 0.000105
White chromaticity D50
Transfer function see below
White luminance 160.0 to 640.0 cd/m2
Black luminance See text
Image state display-referred
Percentages Allowed for R, G and B
Percent reference range  for R,G,B: 0% = 0.0, 100% = 1.0
const E = 16/512;
let sign = c < 0? -1 : 1;
let abs = Math.abs(c);

if (abs <= E) {
  cl =  c / 16;
}
else {
  cl = sign * Math.pow(c, 1.8);
}

c is the gamma-encoded red, green or blue component. cl is the corresponding linear-light component.

diagram of prophoto primaries and secondaries in LCH
Visualization of the prophoto-rgb color space in LCH. The primaries and secondaries are shown (but in sRGB, not in the correct colors). For comparison, the sRGB primaries and secondaries are also shown, as dashed circles. prophoto-rgb primaries and secondaries have much higher Chroma, but much of this ultrawide gamut does not correspond to physically realizable colors.
Tests

10.7. Predefined rec2020 space

rec2020
The rec2020 [Rec.2020] color space accepts three numeric parameters, representing the red, green, and blue channels of the color. In-gamut colors have all three components in the range [0, 1], ("full-range", in video terminology). ITU Reference 2020 is used for Ultra High Definition, 4k and 8k television.

The primaries are physically realizable, but with difficulty as they lie very close to the spectral locus.

Current displays are unable to reproduce the full gamut of rec2020. Coverage is expected to increase over time as displays improve.

It has the following characteristics:

x y
Red chromaticity 0.708 0.292
Green chromaticity 0.170 0.797
Blue chromaticity 0.131 0.046
White chromaticity D65
Transfer function see below, from [Rec.2020] table 4
Image state display-referred
Percentages Allowed for R, G and B
Percent reference range  for R,G,B: 0% = 0.0, 100% = 1.0
const α = 1.09929682680944 ;
const β = 0.018053968510807;

let sign = c < 0? -1 : 1;
let abs = Math.abs(c);

if (abs < β * 4.5 ) {
  cl = c / 4.5;
}
else {
  cl = sign * (Math.pow((abs + α -1 ) / α, 1/0.45));
}

c is the gamma-encoded red, green or blue component. cl is the corresponding linear-light component.

diagram of rec2020 primaries and secondaries in LCH
Visualization of the rec2020 color space in LCH. The primaries and secondaries are shown (but in sRGB, not in the correct colors). For comparison, the sRGB primaries and secondaries are also shown, as dashed circles. rec2020 primaries have much higher Chroma.
Tests

10.8. Predefined CIE XYZ space

xyz-d50, xyz-d65, xyz
The xyz color space accepts three numeric parameters, representing the X,Y and Z values. It represents the CIE XYZ [COLORIMETRY] color space, scaled such that diffuse white has a luminance (Y) of 1.0. and, if necessary, chromatically adapted to the reference white.

The reference white for xyz-d50 is D50, while the reference white for xyz-d65 and xyz is D65.

Values greater than 1.0 are allowed and must not be clamped; they represent colors brighter than diffuse white. Values less than 0 are uncommon, but can occur as a result of chromatic adaptation, and likewise must not be clamped.

It has the following characteristics:

Percentages Allowed for X,Y,Z
Percent reference range  for X,Y,Z: 0% = 0.0, 100% = 1.0
These are exactly equivalent:
 #7654CD
 rgb(46.27% 32.94% 80.39%)
 lab(44.36% 36.05 -58.99)
 color(xyz-d50 0.2005 0.14089 0.4472)
 color(xyz-d65 0.21661 0.14602 0.59452)
Tests

10.9. Converting predefined color spaces to Lab or OKLab

For all predefined RGB color spaces, conversion to Lab requires several steps, although in practice all but the first step are linear calculations and can be combined.

  1. Convert from gamma-encoded RGB to linear-light RGB (undo gamma encoding)
  2. Convert from linear RGB to CIE XYZ
  3. If needed, convert from a D65 whitepoint (used by sRGB, display-p3, a98-rgb and rec2020) to the D50 whitepoint used in Lab, with the Bradford transform. prophoto-rgb already has a D50 whitepoint.
  4. Convert D50-adapted XYZ to Lab

Conversion to OKLab is similar, but the chromatic adaptation step is only needed for prophoto-rgb.

  1. Convert from gamma-encoded RGB to linear-light RGB (undo gamma encoding)
  2. Convert from linear RGB to CIE XYZ
  3. If needed, convert from a D50 whitepoint (used by prophoto-rgb) to the D65 whitepoint used in OKLab, with the Bradford transform.
  4. Convert D65-adapted XYZ to OKLab

There is sample JavaScript code for these conversions in § 15 Sample code for color conversions.

10.10. Converting Lab or OKLab to predefined RGB color spaces

Conversion from Lab to predefined spaces like display-p3 or rec2020 also requires multiple steps, and again in practice all but the last step are linear calculations and can be combined.

  1. Convert Lab to (D50-adapted) XYZ
  2. If needed, convert from a D50 whitepoint (used by Lab) to the D65 whitepoint used in sRGB and most other RGB spaces, with the Bradford transform. prophoto-rgb' does not require this step.
  3. Convert from (D65-adapted) CIE XYZ to linear RGB
  4. Convert from linear-light RGB to RGB (do gamma encoding)

Conversion from OKLab is similar, but the chromatic adaptation step is only needed for prophoto-rgb.

  1. Convert OKLab to (D65-adapted) XYZ
  2. If needed, convert from a D65 whitepoint (used by OKLab) to the D50 whitepoint used in prophoto-rgb, with the Bradford transform.
  3. Convert from (D65-adapted) CIE XYZ to linear RGB
  4. Convert from linear-light RGB to RGB (do gamma encoding)

There is sample JavaScript code for these conversions in § 15 Sample code for color conversions.

Implementations may choose to implement these steps in some other way (for example, using an ICC profile with relative colorimetric rendering intent) provided the results are the same for colors inside both the source and destination gamuts.

10.11. Converting between predefined RGB color spaces

Conversion from one predefined RGB color space to another requires multiple steps, one of which is only needed when the whitepoints differ. To convert from src to dest:

  1. Convert from gamma-encoded srcRGB to linear-light srcRGB (undo gamma encoding)
  2. Convert from linear srcRGB to CIE XYZ
  3. If src and dest have different whitepoints, convert the XYZ value from srcWhite to destWhite with the Bradford transform.
  4. Convert from CIE XYZ to linear destRGB
  5. Convert from linear-light destRGB to destRGB (do gamma encoding)

There is sample JavaScript code for this conversion for the predefined RGB color spaces, in § 15 Sample code for color conversions.

11. Transparency: the opacity property

Opacity can be thought of as a postprocessing operation. Conceptually, after the element (including its descendants) is rendered into an RGBA offscreen image, the opacity setting specifies how to blend the offscreen rendering into the current composite rendering. See simple alpha compositing for details.

Name: opacity
Value: <alpha-value>
Initial: 1
Applies to: all elements
Inherited: no
Percentages: map to the range [0,1]
Computed value: specified number, clamped to the range [0,1]
Canonical order: per grammar
Animation type: by computed value type
Tests
<alpha-value>
The opacity to be applied to the element. It is interpreted identically to its definition in rgb(), except that the resulting opacity is applied to the entire element, rather than a particular color.
Tests

The opacity property applies the specified opacity to the element as a whole, including its contents, rather than applying it to each descendant individually. This means that, for example, an opaque child occluding part of the element’s background will continue to do so even when opacity is less than 1, but the element and child as a whole will show the underlying page through themselves.

It also means that the glyphs corresponding to all characters in the element are treated as a whole; any overlapping portions do not increase the opacity.

Tests
overlapping glyphs rendered correctly, and incorrectly
Correct and incorrect rendering of text with an opacity value of less than one, whose glyphs overlap.

If separate opacity for each glyph is desired, it can be achieved by using a color value which includes opacity, rather than setting the opacity property.

If a box has opacity less than 1, it forms a stacking context for its children. (This prevents its contents from interleaving in the z-axis with content outside it.)

Tests

Furthermore, if the z-index property applies to the box, the auto value is treated as 0 for the element; it is otherwise painted on the same layer within its parent stacking context as positioned elements with stack level 0 (as if it were a positioned element with z-index:0).

See section 9.9 and Appendix E of [CSS2] for more information on stacking contexts.

These rules about z-order do not apply to SVG elements, since SVG has its own rendering model ([SVG11], Chapter 3).

11.1. Simple alpha compositing

When drawing, implementations must handle alpha according to the rules in Section 5.1 Simple alpha compositing of [Compositing].

12. Interpolation

Color interpolation happens with gradients, compositing, filters, transitions, animations, and color mixing and color modification functions.

In general, interpolation between <color> values occurs by linearly interpolating each component of the computed value of the color separately, in a given color space which will be referred to as the 'interpolation space' below.

Interpolating to or from currentcolor is possible. The numerical value used for this purpose is the used value.

Computed value needs to be able to represent combinations of currentColor and an actual color. Consider the value of text-emphasis-color in div { text-emphasis: circle; transition: all 2s; }
div:hover { text-emphasis-color: lime; }
em { color: red; }
See Issue 445.

12.1. Color space for interpolation

Various features in CSS depend on interpolating colors. Examples include:

Mixing or otherwise combining colors has different results depending on the color space used. Thus, different color spaces may be more appropriate for each interpolation use case.

These features are collectively termed the host syntax. The host syntax should define what the default interpolation space should be for each case, and optionally provide syntax for authors to override this default. If such syntax is part of a property value, it should use a <color-interpolation-method> token, defined below for easy reference from other specifications. This ensures consistency across CSS, and that further customizations on how color interpolation is performed can automatically percolate across all of CSS.

<color-space> = <rectangular-color-space> | <polar-color-space>
<rectangular-color-space> = srgb | srgb-linear | lab | oklab | xyz | xyz-d50 | xyz-d65
<polar-color-space> = hsl | hwb | lch | oklch
<hue-interpolation-method> = [ shorter | longer | increasing | decreasing | specified ] hue
<color-interpolation-method> = in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]

The keywords in the definitions of <rectangular-color-space> and <polar-color-space> each refer to their corresponding color space, represented in CSS either by the functional syntax with the same name, or (if no such function is present), by the corresponding <ident> in the color() function.

Tests

If the host syntax does not define what color space interpolation should take place in, it defaults to OKLab.

However, user agents may handle interpolation between legacy sRGB color formats (hex colors, named colors, rgb(), hsl() or hwb() and the equivalent alpha-including forms) in gamma-encoded sRGB space. This provides Web compatibility; legacy sRGB content interpolates in the sRGB space by default.

This also means that authors can choose to opt-in to better interpolation, even between sRGB colors, by using the non-legacy color(srgb r g b) form for at least one of their colors.

If the colors to be interpolated are outside the gamut of the interpolation space, then once converted to that space, they will contain out of range values.

These are not clipped, but the values are interpolated as-is.

12.2. Interpolating with missing components

If a color with a missing component is interpolated with another color which is not missing that component, the missing component is treated as having the other color’s component value.

For example, if these two colors are interpolated, the second of which has a missing hue:
 oklch(78.3% 0.108 326.5)
 oklch(39.2% 0.4 none)

Then the actual colors to be interpolated are

 oklch(78.3% 0.108 326.5)
 oklch(39.2% 0.4 326.5)

and not

 oklch(78.3% 0.108 326.5)
oklch(39.2% 0.4 0)

If both colors are missing a given component, the interpolated color will also be missing that component.

Tests

12.3. Interpolating with alpha

When the colors to be interpolated are not fully opaque, they are transformed into premultiplied color values as follows:

To obtain a color value from a premultipled color value,

Tests
Why is premultiplied alpha useful?

Interpolating colors using the premultiplied representations tends to produce more attractive transitions than the non-premultiplied representations, particularly when transitioning from a fully opaque color to fully transparent.

Note that transitions where either the transparency or the color are held constant (for example, transitioning between rgba(255, 0, 0, 100%) (opaque red) and rgba(0,0,255,100%) (opaque blue), or rgba(255,0,0,100%) (opaque red) and rgba(255,0,0,0%) (transparent red)) have identical results whether the color interpolation is done in premultiplied or non-premultiplied color-space. Differences only arise when both the color and transparency differ between the two endpoints.

The following example illustrates the difference between a gradient transitioning via pre-multiplied values (in this case sRGB, since all colors involved are legacy colors) and one transitioning (incorrectly) via non-premultiplied values. In both of these examples, the gradient is drawn over a white background. Both gradients could be written with the following value:
linear-gradient(90deg, red, transparent, blue)

With premultiplied colors, transitions to or from "transparent" always look nice:

(Image requires SVG)

On the other hand, if a gradient were to incorrectly transition in non-premultiplied space, the center of the gradient would be a noticeably grayish color, because "transparent" is actually a shorthand for rgba(0,0,0,0), or transparent black, meaning that the red transitions to a black as it loses opacity, and similarly with the blue’s transition:

(Image requires SVG)
For example, to interpolate, in the sRGB color space, the two sRGB colors rgb(24% 12% 98% / 0.4) and rgb(62% 26% 64% / 0.6) they would first be converted to premultiplied form [9.6% 4.8% 39.2% ] and [37.2% 15.6% 38.4%] before interpolation.

The midpoint of linearly interpolating these colors would be [23.4% 10.2% 38.8%] which, with an alpha value of 0.5, is rgb(46.8% 20.4% 77.6% / 0.5) when premultiplication is undone.

To interpolate, in the Lab color space, the two colors rgb(76% 62% 03% / 0.4) and color(display-p3 0.84 0.19 0.72 / 0.6) they are first converted to lab lab(66.927% 4.873 68.622 / 0.4) lab(53.503% 82.672 -33.901 / 0.6) then the L, a and b coordinates are premultiplied before interpolation [26.771% 1.949 27.449] and [32.102% 49.603 -20.341].

The midpoint of linearly interpolating these would be [29.4365% 25.776 3.554] which, with an alpha value of 0.5, is lab(58.873% 51.552 7.108) / 0.5) when premultiplication is undone.

To interpolate, in the chroma-preserving LCH color space, the same two colors rgb(76% 62% 03% / 0.4) and color(display-p3 0.84 0.19 0.72 / 0.6) they are first converted to LCH lch(66.93% 68.79 85.94 / 0.4) lch(53.5% 89.35 337.7 / 0.6) then the L and C coordinates (but not H) are premultiplied before interpolation [26.771% 27.516 85.94] and [32.102% 53.61 337.7].

The midpoint of linearly interpolating these, along the shorter hue arc (the default) would be [29.4365% 40.563 31.82] which, with an alpha value of 0.5, is lch(58.873% 81.126 63.64) / 0.5) when premultiplication is undone.

There is sample JavaScript code for alpha premultiplication and un-premultiplication, for both polar and rectangular color spaces, in § 15 Sample code for color conversions.

12.4. Hue interpolation

For color functions with a hue angle (LCH, HSL, HWB etc), there are multiple ways to interpolate. We typically want to avoid arcs over 360 for the difference between the angles, as they are rarely desirable, so in most cases angles are fixed up prior to interpolation so that per-component interpolation is done over less than 360 degrees, often less than 180.

Host syntax can specify any of the following algorithms for hue interpolation (angles in the following are in degrees, but the logic is the same regardless of how they are specified). Specifying a hue interpolation strategy is already part of the <color-interpolation-method> syntax via the <hue-interpolation-method> token.

Unless the type of hue interpolation is specified, both angles need to be constrained to [0, 360) prior to interpolation. One way to do this is θ = ((θ % 360) + 360) % 360.

Unless otherwise specified, if no specific hue interpolation algorithm is selected by the host syntax, the default is shorter.

Tests

Note: As a reminder, if the interpolating colors were not already in the specified interpolation syntax, then converting them will turn any powerless components into missing components.

12.4.1. shorter

Angles are adjusted so that θ₂ - θ₁ ∈ [-180, 180]. In pseudo-Javascript:

if (θ₂ - θ₁ > 180) {
  θ₁ += 360;
}
else if (θ₂ - θ₁ < -180) {
  θ₂ += 360;
}

12.4.2. longer

Angles are adjusted so that θ₂ - θ₁ ∈ {0, [180, 360)}. In pseudo-Javascript:

if (0 < θ₂ - θ₁ < 180) {
  θ₁ += 360;
}
else if (-180 < θ₂ - θ₁ < 0) {
  θ₂ += 360;
}

12.4.3. increasing

Angles are adjusted so that θ₂ - θ₁ ∈ [0, 360). In pseudo-Javascript:

if (θ₂ < θ₁) {
  θ₂ += 360;
}

12.4.4. decreasing

Angles are adjusted so that θ₂ - θ₁ ∈ (-360, 0]. In pseudo-Javascript:

if (θ₁ < θ₂) {
  θ₁ += 360;
}

12.4.5. specified

No fixup is performed. Angles are interpolated in the same way as every other component.

13. Gamut Mapping

13.1. An Introduction to Gamut Mapping

This section is non-normative

When a color in an origin color space is converted to another, destination color space which has a smaller gamut, some colors will be outside the destination gamut.

For intermediate color calculations, these out of gamut values are preserved. However, if the destination is the display device (a screen, or a printer) then out of gamut values must be converted to an in-gamut color.

Gamut mapping is the process of finding an in-gamut color with the least objectionable change in visual apprearance.

13.1.1. Clipping

The simplest and least acceptable method is simply to clip the component values to the displayable range. This changes the proportions of the three primary colors (for an RGB display), resulting in a hue shift.

For example, consider the color color(srgb-linear 0.5 1 3). Because this is a linear-light color space, we can compare the intensities of the three components and see that the amount of blue light is three times the amount of green, while the amount of red light is half that of green. There is six times as much blue primary as red. In OKLCH, this color has a hue angle of 265.1°

If we now clip this color to bring it into gamut for sRGB, we get color(srgb-linear 0.5 1 1). The amount of blue light is the same as green. In OKLCH, this color has a hue angle of 196.1°, a substantial change of 69°.

13.1.2. Closest color (MINDE)

A better method is to map colors, in a perceptually uniform color space, by finding the closest in-gamut color (so-called minimum ΔE or MINDE). Clearly, the sucess of this technique depends on the degree of uniformity of the gamut mapping color space and the predictive accuracy of the deltaE function used.

However, when doing gamut mapping changes in Hue are particularly objectionable; changes in Chroma are more tolerable, and small changes in Lightness can also be acceptable especially if the alternative is a larger Chroma reduction. MINDE weights changes in each dimension equally, and thus gives suboptimal results.

13.1.3. Chroma reduction

To improve on MINDE algorithms, colors are mapped in a perceptually uniform, polar color space by holding the hue constant, and reducing the chroma until the color falls in gamut.

In this example, Display P3 primary yellow (color(display-p3 1 1 0)) is being mapped to an sRGB display. The gamut mapping color space is OKLCH.
color(display-p3 1 1 0)

is

color(srgb 1 1 -0.3463)

which is

color(oklch 0.96476 0.24503 110.23)

By progressively reducing the chroma component until the resulting color falls inside the sRGB gamut (has no components negative, or greater than one) a gamut mapped color is obtained.

   color(oklch 0.96476 0.21094 110.23)

which is

   color(srgb 0.99116 0.99733 0.00001)
A constant-hue slice of OKLCH color space. The vertical axis represents lightness, the horizontal axis is chroma. The color to be mapped, shown as a yellow circle, has the chroma reduced while keeping hue and lightness constant. The color therefore moves along the maroon line in the diagram, towards the neutral axis on the left. The gamut boundary of sRGB is shown in green.

A practical implementation will converge more quickly than a linear reduction; either by binary search, or by computing an analytical intersection of the line of constant hue and lightness with the gamut boundary.

13.1.4. Excessive chroma reduction

Also, this simple approach will give sub-optimal results for certain colors, principally very light colors like yellow and cyan, if the upper edge of the gamut boundary is shallow, or even slightly concave. The line of constant lightness can skim just above the gamut boundary, resulting in an excessively low chroma in those cases.

The choice of color space will affect the acceptability of the gamut mapped colors.

In this example, Display P3 primary yellow (color(display-p3 1 1 0) has the chroma progressively reduced in CIE LCH color space.
In the upper part of this diagram, colors which are inside the gamut of sRGB are displayed as-is. Colors inside the gamut of Display P3 (but outside sRGB) are in salmon. Colors outside the gamut of Display P3 are in red. The lower part of the diagram shows the linear-light intensities of the Display P3 red, green and blue components.
It can be seen that reduction in CIE LCH chroma makes the red intensity curve up, out of Display P3 gamut; by the time it falls again the chroma is very low. Simple gamut mapping in CIE LCH would give unsatisfactory results.
In this example, Display P3 primary yellow (color(display-p3 1 1 0) has the chroma progressively reduced, but this time in OKLCH color space.
In the upper part of this diagram, colors which are inside the gamut of sRGB are displayed as-is. Colors inside the gamut of Display P3 (but outside sRGB) are in salmon. Colors outside the gamut of Display P3 are in red. The lower part of the diagram shows the linear-light intensities of the Display P3 red, green and blue components.
It can be seen that reduction in OKLCH chroma is better behaved. Colors do not go outside the Display P3 gamut, and the resulting gamut-mapped yellow has good chroma. Simple gamut mapping in CIE LCH would give acceptable results.

13.1.5. Chroma reduction with local clipping

The simple chroma-reduction algorithm can be improved: at each step, the color difference is computed between the current mapped color and a clipped version of that color. If the current color is outside the gamut boundary, but the color difference between it and the clipped version is below the threshold for a just noticeable difference (JND), the clipped version of the color is returned as the mapped result. Effectively, this is doing a MINDE mapping at each stage, but constrained so the hue and lightness changes are very small, and thus are not noticeable.

In this example, Display P3 primary yellow (color(display-p3 1 1 0) has the chroma progressively reduced in CIE LCH color space, with the local clip modification.
In the upper part of this diagram, colors which are inside the gamut of sRGB are displayed as-is. Colors inside the gamut of Display P3 (but outside sRGB) are in salmon. Colors outside the gamut of Display P3 are in red. The lower part of the diagram shows the linear-light intensities of the Display P3 red, green and blue components.
It can be seen that reduction in CIE LCH chroma still makes the red intensity curve up, out of Display P3 gamut; but less than before and the sRGB boundary is found much more quickly. Gamut mapping in CIE LCH with local clip would give acceptable results.
In this example, Display P3 primary yellow (color(display-p3 1 1 0) has the chroma progressively reduced, but this time in OKLCH color space and with the local clip modification.
In the upper part of this diagram, colors which are inside the gamut of sRGB are displayed as-is. Colors inside the gamut of Display P3 (but outside sRGB) are in salmon. Colors outside the gamut of Display P3 are in red. The lower part of the diagram shows the linear-light intensities of the Display P3 red, green and blue components.
It can be seen that reduction in OKLCH chroma, which was already good, is further improved by the local clip modification. Simple gamut mapping in CIE LCH with local clip would give excellent results.

13.1.6. Deviations from perceptual uniformity: hue curvature

Using the CIE LCH color space and deltaE2000 distance metric, is known to give suboptimal results with significant hue shifts, for colors in the hue range 270° to 330°.

A constant-hue slice of CIE LCH color space, at a hue angle of 301.37° corresponding to sRGB primary blue. The vertical axis is Lightness, the horizontal axis is Chroma. Between chroma of 25 and 75, the hue is visibly purple, becoming more blue between 100 and 131. The same phenomenon continues past 131, but cannot be shown on an sRGB display.

Using OKLCH color space and deltaEOK distance metric avoids this issue at all hue angles.

A constant-hue slice of OKLCH color space, at a hue angle of 264.06° corresponding to sRGB primary blue. The vertical axis is Lightness, the horizontal axis is Chroma. The hue is visibly the same at all values of chroma, up to 0.315 (the sRGB limit at this hue). It continues to be constant beyond this point, although that cannot be shown on an sRGB diagram.

13.2. CSS Gamut Mapping to an RGB destination

This CSS gamut mapping algorithm applies to individual, Standard Dynamic Range (SDR) CSS colors which are out of gamut of an RGB display and thus require to be css gamut mapped. It implements a relative colorimetric intent, and colors inside the destination gamut are unchanged.

Note: other situations, in particular mapping to printer gamuts where the maximum black level is significantly above zero, will require different algorithms which align the respective black and white points, which will result in lightness changes for very light and very dark colors as chroma is reduced..

Note: this algorithm is for individual, distinct colors; for color images, where relationships between neighboring pixels are important and the aim is to preserve detail and texture, a perceptual rendering intent is more appropriate and in that case, colors inside the destination gamut could be changed.

CSS gamut mapping occurs in the OKLCH color space, and the color difference formula used is deltaEOK. The local-MINDE improvement is used.

For colors which are out of range on the Lightness axis, white is returned in the destination color space if the Lightness is greater than or equal to 1.0, while black is returned in the destination color space if the Lightness is less than or equal to 0.0.

For the binary search implementation, at each step in the search, the deltaEOK is computed between the current mapped color and a clipped version of that color. If the current color is outside the gamut boundary, but the deltaEOK between it and the clipped version is below a threshold for a just noticeable difference (JND), the clipped version of the color is returned as the mapped result.

For the analytical implementation, having found the exact intersection, project outwards (towards higher chroma) along the line of constant lightness until the deltaEOK between the projected point and a clipped version of that point exceeds one JND. Then returned the clipped version of the color as the mapped result.

For the OKLCH color space, one JND is is an OKLCH difference of 0.02.

13.2.1. Sample pseudocode for the binary search gamut mapping algorithm with local MINDE

To CSS gamut map a color origin in color space origin color space to be in gamut of a destination color space destination:
  1. let origin_OKLCH be origin converted from origin color space to the OKLCH color space
  2. if the Lightness of origin_OKLCH is greater than or equal to 100%, return { 1 1 1 origin.alpha } in destination
  3. if the Lightness of origin_OKLCH is less than than or equal to 0%, return { 0 0 0 origin.alpha } in destination
  4. let inGamut(color) be a function which returns true if, when passed a color, that color is inside the gamut of destination
  5. if inGamut(origin_OKLCH) is true, convert origin_OKLCH to destination and return it as the gamut mapped color
  6. otherwise, let delta(one, two) be a function which returns the deltaEOK of color one compared to color two
  7. let JND be 0.02
  8. let clip(color)| be a function which converts color to destination, converts all negative components to zero, converts all components greater that one to one, and returns the result
  9. set min to zero
  10. set max to the OKLCH chroma of origin_OKLCH
  11. repeat the following steps
    1. set chroma to (min + max) /2
    2. set current to origin_OKLCH and then set the chroma component to chroma
    3. if inGamut(current) is true, set min to chroma and continue to repeat these steps
    4. otherwise, if inGamut(current) is false carry out these steps:
      1. set clipped to clip(current)
      2. set E to delta(clipped, current)
      3. if E < JND return clipped as the gamut mapped color
      4. otherwise, set max to chroma and continue to repeat these steps

14. Default Style Rules

The following stylesheet is informative, not normative. This style sheet could be used by an implementation as part of its default styling of HTML documents.

/* traditional desktop user agent colors for hyperlinks */
:link { color: LinkText; }
:visited { color: VisitedText; }
:active { color: ActiveText; }

15. Sample code for color conversions

This section is not normative.

For clarity, a library is used for matrix multiplication. (This is more readable than inlining all the multiplies and adds). The matrices are in column-major order.

// Sample code for color conversions
// Conversion can also be done using ICC profiles and a Color Management System
// For clarity, a library is used for matrix multiplication (multiply-matrices.js)

// standard white points, defined by 4-figure CIE x,y chromaticities
const D50 = [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585];
const D65 = [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290];

// sRGB-related functions

function lin_sRGB(RGB) {
	// convert an array of sRGB values
	// where in-gamut values are in the range [0 - 1]
	// to linear light (un-companded) form.
	// https://en.wikipedia.org/wiki/SRGB
	// Extended transfer function:
	// for negative values,  linear portion is extended on reflection of axis,
	// then reflected power function is used.
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs < 0.04045) {
			return val / 12.92;
		}

		return sign * (Math.pow((abs + 0.055) / 1.055, 2.4));
	});
}

function gam_sRGB(RGB) {
	// convert an array of linear-light sRGB values in the range 0.0-1.0
	// to gamma corrected form
	// https://en.wikipedia.org/wiki/SRGB
	// Extended transfer function:
	// For negative values, linear portion extends on reflection
	// of axis, then uses reflected pow below that
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs > 0.0031308) {
			return sign * (1.055 * Math.pow(abs, 1/2.4) - 0.055);
		}

		return 12.92 * val;
	});
}

function lin_sRGB_to_XYZ(rgb) {
	// convert an array of linear-light sRGB values to CIE XYZ
	// using sRGB's own white, D65 (no chromatic adaptation)

	var M = [
		[ 0.41239079926595934, 0.357584339383878,   0.1804807884018343  ],
		[ 0.21263900587151027, 0.715168678767756,   0.07219231536073371 ],
		[ 0.01933081871559182, 0.11919477979462598, 0.9505321522496607  ]
	];
	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_sRGB(XYZ) {
	// convert XYZ to linear-light sRGB

	var M = [
		[  3.2409699419045226,  -1.537383177570094,   -0.4986107602930034  ],
		[ -0.9692436362808796,   1.8759675015077202,   0.04155505740717559 ],
		[  0.05563007969699366, -0.20397695888897652,  1.0569715142428786  ]
	];

	return multiplyMatrices(M, XYZ);
}

//  display-p3-related functions


function lin_P3(RGB) {
	// convert an array of display-p3 RGB values in the range 0.0 - 1.0
	// to linear light (un-companded) form.

	return lin_sRGB(RGB);	// same as sRGB
}

function gam_P3(RGB) {
	// convert an array of linear-light display-p3 RGB  in the range 0.0-1.0
	// to gamma corrected form

	return gam_sRGB(RGB);	// same as sRGB
}

function lin_P3_to_XYZ(rgb) {
	// convert an array of linear-light display-p3 values to CIE XYZ
	// using  D65 (no chromatic adaptation)
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var M = [
		[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
		[0.2289745640697488, 0.6917385218365064,  0.079286914093745],
		[0.0000000000000000, 0.04511338185890264, 1.043944368900976]
	];
	// 0 was computed as -3.972075516933488e-17

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_P3(XYZ) {
	// convert XYZ to linear-light P3
	var M = [
		[ 2.493496911941425,   -0.9313836179191239, -0.40271078445071684],
		[-0.8294889695615747,   1.7626640603183463,  0.023624685841943577],
		[ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872]
	];

	return multiplyMatrices(M, XYZ);
}

// prophoto-rgb functions

function lin_ProPhoto(RGB) {
	// convert an array of prophoto-rgb values
	// where in-gamut colors are in the range [0.0 - 1.0]
	// to linear light (un-companded) form.
	// Transfer curve is gamma 1.8 with a small linear portion
	// Extended transfer function
	const Et2 = 16/512;
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs <= Et2) {
			return val / 16;
		}

		return sign * Math.pow(val, 1.8);
	});
}

function gam_ProPhoto(RGB) {
	// convert an array of linear-light prophoto-rgb  in the range 0.0-1.0
	// to gamma corrected form
	// Transfer curve is gamma 1.8 with a small linear portion
	// TODO for negative values, extend linear portion on reflection of axis, then add pow below that
	const Et = 1/512;
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs >= Et) {
			return sign * Math.pow(abs, 1/1.8);
		}

		return 16 * val;
	});
}

function lin_ProPhoto_to_XYZ(rgb) {
	// convert an array of linear-light prophoto-rgb values to CIE XYZ
	// using  D50 (so no chromatic adaptation needed afterwards)
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var M = [
		[ 0.7977604896723027,  0.13518583717574031,  0.0313493495815248     ],
		[ 0.2880711282292934,  0.7118432178101014,   0.00008565396060525902 ],
		[ 0.0,                 0.0,                  0.8251046025104601     ]
	];

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_ProPhoto(XYZ) {
	// convert XYZ to linear-light prophoto-rgb
	var M = [
	  	[  1.3457989731028281,  -0.25558010007997534,  -0.05110628506753401 ],
	  	[ -0.5446224939028347,   1.5082327413132781,    0.02053603239147973 ],
	  	[  0.0,                  0.0,                   1.2119675456389454  ]
	];

	return multiplyMatrices(M, XYZ);
}

// a98-rgb functions

function lin_a98rgb(RGB) {
	// convert an array of a98-rgb values in the range 0.0 - 1.0
	// to linear light (un-companded) form.
	// negative values are also now accepted
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

	  	return sign * Math.pow(abs, 563/256);
	});
}

function gam_a98rgb(RGB) {
	// convert an array of linear-light a98-rgb  in the range 0.0-1.0
	// to gamma corrected form
	// negative values are also now accepted
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		return sign * Math.pow(abs, 256/563);
	});
}

function lin_a98rgb_to_XYZ(rgb) {
	// convert an array of linear-light a98-rgb values to CIE XYZ
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	// has greater numerical precision than section 4.3.5.3 of
	// https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
	// but the values below were calculated from first principles
	// from the chromaticity coordinates of R G B W
	// see matrixmaker.html
	var M = [
		[ 0.5766690429101305,   0.1855582379065463,   0.1882286462349947  ],
		[ 0.29734497525053605,  0.6273635662554661,   0.07529145849399788 ],
		[ 0.02703136138641234,  0.07068885253582723,  0.9913375368376388  ]
	];

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_a98rgb(XYZ) {
	// convert XYZ to linear-light a98-rgb
	var M = [
		[  2.0415879038107465,    -0.5650069742788596,   -0.34473135077832956 ],
		[ -0.9692436362808795,     1.8759675015077202,    0.04155505740717557 ],
		[  0.013444280632031142,  -0.11836239223101838,   1.0151749943912054  ]
	];

	return multiplyMatrices(M, XYZ);
}

//Rec. 2020-related functions

function lin_2020(RGB) {
	// convert an array of rec2020 RGB values in the range 0.0 - 1.0
	// to linear light (un-companded) form.
	// ITU-R BT.2020-2 p.4

	const α = 1.09929682680944 ;
	const β = 0.018053968510807;

	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs < β * 4.5 ) {
			return val / 4.5;
		}

		return sign * (Math.pow((abs + α -1 ) / α, 1/0.45));
	});
}

function gam_2020(RGB) {
	// convert an array of linear-light rec2020 RGB  in the range 0.0-1.0
	// to gamma corrected form
	// ITU-R BT.2020-2 p.4

	const α = 1.09929682680944 ;
	const β = 0.018053968510807;


	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs > β ) {
			return sign * (α * Math.pow(abs, 0.45) - (α - 1));
		}

		return 4.5 * val;
	});
}

function lin_2020_to_XYZ(rgb) {
	// convert an array of linear-light rec2020 values to CIE XYZ
	// using  D65 (no chromatic adaptation)
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var M = [
		[0.6369580483012914, 0.14461690358620832,  0.1688809751641721],
		[0.2627002120112671, 0.6779980715188708,   0.05930171646986196],
		[0.000000000000000,  0.028072693049087428, 1.060985057710791]
	];
	// 0 is actually calculated as  4.994106574466076e-17

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_2020(XYZ) {
	// convert XYZ to linear-light rec2020
	var M = [
		[1.7166511879712674,   -0.35567078377639233, -0.25336628137365974],
		[-0.6666843518324892,   1.6164812366349395,   0.01576854581391113],
		[0.017639857445310783, -0.042770613257808524, 0.9421031212354738]
	];

	return multiplyMatrices(M, XYZ);
}

// Chromatic adaptation

function D65_to_D50(XYZ) {
	// Bradford chromatic adaptation from D65 to D50
	// The matrix below is the result of three operations:
	// - convert from XYZ to retinal cone domain
	// - scale components from one reference white to another
	// - convert back to XYZ
	// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
	var M =  [
		[  1.0479298208405488,    0.022946793341019088,  -0.05019222954313557 ],
		[  0.029627815688159344,  0.990434484573249,     -0.01707382502938514 ],
		[ -0.009243058152591178,  0.015055144896577895,   0.7518742899580008  ]
	];

	return multiplyMatrices(M, XYZ);
}

function D50_to_D65(XYZ) {
	// Bradford chromatic adaptation from D50 to D65
	var M = [
		[  0.9554734527042182,   -0.023098536874261423,  0.0632593086610217   ],
		[ -0.028369706963208136,  1.0099954580058226,    0.021041398966943008 ],
		[  0.012314001688319899, -0.020507696433477912,  1.3303659366080753   ]
	];

	return multiplyMatrices(M, XYZ);
}

// CIE Lab and LCH

function XYZ_to_Lab(XYZ) {
	// Assuming XYZ is relative to D50, convert to CIE Lab
	// from CIE standard, which now defines these as a rational fraction
	var ε = 216/24389;  // 6^3/29^3
	var κ = 24389/27;   // 29^3/3^3

	// compute xyz, which is XYZ scaled relative to reference white
	var xyz = XYZ.map((value, i) => value / D50[i]);

	// now compute f
	var f = xyz.map(value => value > ε ? Math.cbrt(value) : (κ * value + 16)/116);

	return [
		(116 * f[1]) - 16, 	 // L
		500 * (f[0] - f[1]), // a
		200 * (f[1] - f[2])  // b
	];
	// L in range [0,100]. For use in CSS, add a percent
}

function Lab_to_XYZ(Lab) {
	// Convert Lab to D50-adapted XYZ
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var κ = 24389/27;   // 29^3/3^3
	var ε = 216/24389;  // 6^3/29^3
	var f = [];

	// compute f, starting with the luminance-related term
	f[1] = (Lab[0] + 16)/116;
	f[0] = Lab[1]/500 + f[1];
	f[2] = f[1] - Lab[2]/200;

	// compute xyz
	var xyz = [
		Math.pow(f[0],3) > ε ?   Math.pow(f[0],3)            : (116*f[0]-16)/κ,
		Lab[0] > κ * ε ?         Math.pow((Lab[0]+16)/116,3) : Lab[0]/κ,
		Math.pow(f[2],3)  > ε ?  Math.pow(f[2],3)            : (116*f[2]-16)/κ
	];

	// Compute XYZ by scaling xyz by reference white
	return xyz.map((value, i) => value * D50[i]);
}

function Lab_to_LCH(Lab) {
	// Convert to polar form
	var hue = Math.atan2(Lab[2], Lab[1]) * 180 / Math.PI;
	return [
		Lab[0], // L is still L
		Math.sqrt(Math.pow(Lab[1], 2) + Math.pow(Lab[2], 2)), // Chroma
		hue >= 0 ? hue : hue + 360 // Hue, in degrees [0 to 360)
	];
}

function LCH_to_Lab(LCH) {
	// Convert from polar form
	return [
		LCH[0], // L is still L
		LCH[1] * Math.cos(LCH[2] * Math.PI / 180), // a
		LCH[1] * Math.sin(LCH[2] * Math.PI / 180) // b
	];
}

// OKLab and OKLCH
// https://bottosson.github.io/posts/oklab/

// XYZ <-> LMS matrices recalculated for consistent reference white
// see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-943521484

function XYZ_to_OKLab(XYZ) {
	// Given XYZ relative to D65, convert to OKLab
	var XYZtoLMS = [
		[ 0.8190224432164319,    0.3619062562801221,   -0.12887378261216414  ],
		[ 0.0329836671980271,    0.9292868468965546,     0.03614466816999844 ],
		[ 0.048177199566046255,  0.26423952494422764,    0.6335478258136937  ]
	];
	var LMStoOKLab = [
		[  0.2104542553,   0.7936177850,  -0.0040720468 ],
		[  1.9779984951,  -2.4285922050,   0.4505937099 ],
		[  0.0259040371,   0.7827717662,  -0.8086757660 ]
	];

	var LMS = multiplyMatrices(XYZtoLMS, XYZ);
	return multiplyMatrices(LMStoOKLab, LMS.map(c => Math.cbrt(c)));
	// L in range [0,1]. For use in CSS, multiply by 100 and add a percent
}

function OKLab_to_XYZ(OKLab) {
	// Given OKLab, convert to XYZ relative to D65
	var LMStoXYZ =  [
		[  1.2268798733741557,  -0.5578149965554813,   0.28139105017721583 ],
		[ -0.04057576262431372,  1.1122868293970594,  -0.07171106666151701 ],
		[ -0.07637294974672142, -0.4214933239627914,   1.5869240244272418  ]
	];
	var OKLabtoLMS = [
        [ 0.99999999845051981432,  0.39633779217376785678,   0.21580375806075880339  ],
        [ 1.0000000088817607767,  -0.1055613423236563494,   -0.063854174771705903402 ],
        [ 1.0000000546724109177,  -0.089484182094965759684, -1.2914855378640917399   ]
    ];

	var LMSnl = multiplyMatrices(OKLabtoLMS, OKLab);
	return multiplyMatrices(LMStoXYZ, LMSnl.map(c => c ** 3));
}

function OKLab_to_OKLCH(OKLab) {
	var hue = Math.atan2(OKLab[2], OKLab[1]) * 180 / Math.PI;
	return [
		OKLab[0], // L is still L
		Math.sqrt(OKLab[1] ** 2 + OKLab[2] ** 2), // Chroma
		hue >= 0 ? hue : hue + 360 // Hue, in degrees [0 to 360)
	];
}

function OKLCH_to_OKLab(OKLCH) {
	return [
		OKLCH[0], // L is still L
		OKLCH[1] * Math.cos(OKLCH[2] * Math.PI / 180), // a
		OKLCH[1] * Math.sin(OKLCH[2] * Math.PI / 180)  // b
	];
}

// Premultiplied alpha conversions

function rectangular_premultiply(color, alpha) {
// given a color in a rectangular orthogonal colorspace
// and an alpha value
// return the premultiplied form
	return color.map((c) => c * alpha)
}

function rectangular_un_premultiply(color, alpha) {
// given a premultiplied color in a rectangular orthogonal colorspace
// and an alpha value
// return the actual color
	if (alpha === 0) {
		return color; // avoid divide by zero
	}
	return color.map((c) => c / alpha)
}

function polar_premultiply(color, alpha, hueIndex) {
	// given a color in a cylindicalpolar colorspace
	// and an alpha value
	// return the premultiplied form.
	// the index says which entry in the color array corresponds to hue angle
	// for example, in OKLCH it would be 2
	// while in HSL it would be 0
	return color.map((c, i) => c * (hueIndex === i? 1 : alpha))
}

function polar_un_premultiply(color, alpha, hueIndex) {
	// given a color in a cylindicalpolar colorspace
	// and an alpha value
	// return the actual color.
	// the hueIndex says which entry in the color array corresponds to hue angle
	// for example, in OKLCH it would be 2
	// while in HSL it would be 0
	if (alpha === 0) {
		return color; // avoid divide by zero
	}
	return color.map((c, i) => c / (hueIndex === i? 1 : alpha))
}

// Convenience functions can easily be defined, such as
function hsl_premultiply(color, alpha) {
	return polar_premultiply(color, alpha, 0);
}

16. Sample code for ΔE2000 and ΔEOK color differences

This section is not normative.

16.1. ΔE2000

The simplest color difference metric, ΔE76, is simply the Euclidean distance in Lab color space. While this is a good first approximation, color-critical industries such as printing and fabric dyeing soon developed improved formulae. Currently, the most widely used formula is ΔE2000. It corrects a number of known asymmetries and non-linearities compared to ΔE76. Because the formula is complex, and critically dependent on the sign of various intermediate calculations, implementations are often incorrect [Sharma].

The sample code below has been validated to five significant figures against the test suite of paired Lab values and expected ΔE2000 published by [Sharma] and is correct.

// deltaE2000 is a statistically significant improvement
// over deltaE76 and deltaE94,
// and is recommended by the CIE and Idealliance
// especially for color differences less than 10 deltaE76
// but is wicked complicated
// and many implementations have small errors!


function deltaE2000 (reference, sample) {

    // Given a reference and a sample color,
    // both in CIE Lab,
    // calculate deltaE 2000.

    // This implementation assumes the parametric
    // weighting factors kL, kC and kH
    // (for the influence of viewing conditions)
    // are all 1, as seems typical.

    let [L1, a1, b1] = reference;
    let [L2, a2, b2] = sample;
    let C1 = Math.sqrt(a1 ** 2 + b1 ** 2);
    let C2 = Math.sqrt(a2 ** 2 + b2 ** 2);

	let Cbar = (C1 + C2)/2; // mean Chroma

	// calculate a-axis asymmetry factor from mean Chroma
	// this turns JND ellipses for near-neutral colors back into circles
	let C7 = Math.pow(Cbar, 7);
	const Gfactor = Math.pow(25, 7);
	let G = 0.5 * (1 - Math.sqrt(C7/(C7+Gfactor)));

	// scale a axes by asymmetry factor
	// this by the way is why there is no Lab2000 color space
	let adash1 = (1 + G) * a1;
	let adash2 = (1 + G) * a2;

	// calculate new Chroma from scaled a and original b axes
	let Cdash1 = Math.sqrt(adash1 ** 2 + b1 ** 2);
	let Cdash2 = Math.sqrt(adash2 ** 2 + b2 ** 2);

	// calculate new hues, with zero hue for true neutrals
	// and in degrees, not radians
	const π = Math.PI;
	const r2d = 180 / π;
	const d2r = π / 180;
	let h1 = (adash1 === 0 && b1 === 0)? 0: Math.atan2(b1, adash1);
	let h2 = (adash2 === 0 && b2 === 0)? 0: Math.atan2(b2, adash2);

	if (h1 < 0) {
		h1 += 2 * π;
	}
	if (h2 < 0) {
		h2 += 2 * π;
	}

	h1 *= r2d;
	h2 *= r2d;

	// Lightness and Chroma differences; sign matters
	let ΔL = L2 - L1;
	let ΔC = Cdash2 - Cdash1;

	// Hue difference, taking care to get the sign correct
	let hdiff = h2 - h1;
	let hsum = h1 + h2;
	let habs = Math.abs(hdiff);
	let Δh;

	if (Cdash1 * Cdash2 === 0) {
		Δh = 0;
	}
	else if (habs <= 180) {
		Δh = hdiff;
	}
	else if (hdiff > 180) {
		Δh = hdiff - 360;
	}
	else if (hdiff < -180) {
		Δh = hdiff + 360;
	}
	else {
		console.log("the unthinkable has happened");
	}

	// weighted Hue difference, more for larger Chroma
	let ΔH = 2 * Math.sqrt(Cdash2 * Cdash1) * Math.sin(Δh * d2r / 2);

	// calculate mean Lightness and Chroma
	let Ldash = (L1 + L2)/2;
	let Cdash = (Cdash1 + Cdash2)/2;
	let Cdash7 = Math.pow(Cdash, 7);

	// Compensate for non-linearity in the blue region of Lab.
	// Four possibilities for hue weighting factor,
	// depending on the angles, to get the correct sign
	let hdash;
	if (Cdash1 == 0 && Cdash2 == 0) {
		hdash = hsum;   // which should be zero
	}
	else if (habs <= 180) {
		hdash = hsum / 2;
	}
	else if (hsum < 360) {
		hdash = (hsum + 360) / 2;
	}
	else {
		hdash = (hsum - 360) / 2;
	}

	// positional corrections to the lack of uniformity of CIELAB
	// These are all trying to make JND ellipsoids more like spheres

	// SL Lightness crispening factor
	// a background with L=50 is assumed
	let lsq = (Ldash - 50) ** 2;
	let SL = 1 + ((0.015 * lsq) / Math.sqrt(20 + lsq));

	// SC Chroma factor, similar to those in CMC and deltaE 94 formulae
	let SC = 1 + 0.045 * Cdash;

	// Cross term T for blue non-linearity
	let T = 1;
	T -= (0.17 * Math.cos((     hdash - 30)  * d2r));
	T += (0.24 * Math.cos(  2 * hdash        * d2r));
	T += (0.32 * Math.cos(((3 * hdash) + 6)  * d2r));
	T -= (0.20 * Math.cos(((4 * hdash) - 63) * d2r));

	// SH Hue factor depends on Chroma,
	// as well as adjusted hue angle like deltaE94.
	let SH = 1 + 0.015 * Cdash * T;

	// RT Hue rotation term compensates for rotation of JND ellipses
	// and Munsell constant hue lines
	// in the medium-high Chroma blue region
	// (Hue 225 to 315)
	let Δθ = 30 * Math.exp(-1 * (((hdash - 275)/25) ** 2));
	let RC = 2 * Math.sqrt(Cdash7/(Cdash7 + Gfactor));
	let RT = -1 * Math.sin(2 * Δθ * d2r) * RC;

	// Finally calculate the deltaE, term by term as root sum of squares
	let dE = (ΔL / SL) ** 2;
	dE += (ΔC / SC) ** 2;
	dE += (ΔH / SH) ** 2;
	dE += RT * (ΔC / SC) * (ΔH / SH);
	return Math.sqrt(dE);
	// Yay!!!
};

16.2. ΔEOK

Because OKLab does not suffer from the hue linearity, hue uniformity, and chroma non-linearities of CIE Lab, the color difference metric does not need to correct for them and so is simply the Euclidean distance in OKLab color space.

// Calculate deltaE OK
// simple root sum of squares

function deltaEOK (reference, sample) {
// Given reference and sample are both in OKLab
    let [L1, a1, b1] = reference;
	let [L2, a2, b2] = sample;
	let ΔL = L1 - L2;
	let Δa = a1 - a2;
	let Δb = b1 - b2;
	return Math.sqrt(ΔL ** 2 + Δa ** 2 + Δb ** 2);
}

Appendix A: Deprecated CSS System Colors

Earlier versions of CSS defined several additional system colors. These color keywords have been deprecated, however, as they are insufficient for their original purpose (making website elements look like their native OS counterparts), represent a security risk by making it easier for a webpage to “spoof” a native OS dialog, and increase fingerprinting surface, compromising user privacy.

User agents must support these keywords, and to mitigate fingerprinting must map them to the (undeprecated) system colors as listed below. Authors must not use these keywords.

The deprecated system colors are represented as the <deprecated-color> sub-type, and are defined as:

ActiveBorder
Active window border. Same as ButtonBorder.
ActiveCaption
Active window caption. Same as CanvasText.
AppWorkspace
Background color of multiple document interface. Same as Canvas.
Background
Desktop background. Same as Canvas.
ButtonHighlight
The color of the border facing the light source for 3-D elements that appear 3-D due to one layer of surrounding border. Same as ButtonFace.
ButtonShadow
The color of the border away from the light source for 3-D elements that appear 3-D due to one layer of surrounding border. Same as ButtonFace.
CaptionText
Text in caption, size box, and scrollbar arrow box. Same as CanvasText.
InactiveBorder
Inactive window border. Same as ButtonBorder.
InactiveCaption
Inactive window caption. Same as Canvas.
InactiveCaptionText
Color of text in an inactive caption. Same as GrayText.
InfoBackground
Background color for tooltip controls. Same as Canvas.
InfoText
Text color for tooltip controls. Same as CanvasText.
Menu
Menu background. Same as Canvas.
MenuText
Text in menus. Same as CanvasText.
Scrollbar
Scroll bar gray area. Same as Canvas.
ThreeDDarkShadow
The color of the darker (generally outer) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
ThreeDFace
The face background color for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonFace.
ThreeDHighlight
The color of the lighter (generally outer) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
ThreeDLightShadow
The color of the darker (generally inner) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
ThreeDShadow
The color of the lighter (generally inner) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
Window
Window background. Same as Canvas.
WindowFrame
Window frame. Same as ButtonBorder.
WindowText
Text in windows. Same as CanvasText.
Tests

Results on these 'same as' equivalence tests are not great, which is why the feature is at-risk


Appendix B: Deprecated Quirky Hex Colors

When CSS is being parsed in quirks mode, <quirky-color> is a type of <color> that is only valid in certain properties:

It is not valid in properties that include or reference these properties, such as the background shorthand, or inside functional notations such as color-mix()

Additionally, while <quirky-color> must be valid as a <color> when parsing the affected properties in the @supports rule, it is not valid for those properties when used in the CSS.supports() method.

A <quirky-color> can be represented as a <number-token>, <dimension-token>, or <ident-token>, according to the following rules:

(In other words, Quirks Mode allows hex colors to be written without the leading "#", but with weird parsing rules.)

Acknowledgments

In addition to those who contributed to CSS Color 3, the editors would like to thank Emilio Cobos Álvarez, Chris Bai, Amelia Bellamy-Royds, Lars Borg, Mike Bremford, Andreu Botella, Dan Burzo, Max Derhak, fantasai, Simon Fraser, Phil Green, Dean Jackson, Andreas Kraushaar, Pierre-Anthony Lemieux, Cameron McCormack, Romain Menke, Chris Murphy, Isaac Muse, Jonathan Neal, Chris Needham, Christoph Päper, Brad Pettit, Xidorn Quan, Craig Revie, Melanie Richards, Florian Rivoal, Jacob Rus, Joseph Salowey, Simon Sapin, Igor Snitkin, Lea Verou, Mark Watson, Sam Weinig, and Natalie Weizenbaum.

Changes

Changes since the Working Draft of 15 December 2021

Changes since the Working Draft of 1 June 2021

Changes since the Working Draft of 12 November 2020

Changes since Working Draft of 5 November 2019

Changes since Working Draft of 05 July 2016

Changes from Colors 3

The primary change, compared to CSS Color 3, is that CSS colors are no longer restricted to the narrow gamut of sRGB.

To support this, several brand new features have been added:

  1. predefined, wide color gamut RGB color spaces
  2. lab(), lch(), oklab() and oklch() functions, for device-independent color

Other technical changes:

  1. Serialization of <color> is now specified here, rather than in the CSS Object Model
  2. hwb() function, for specifying sRGB colors in the HWB notation.
  3. Addition of named color rebeccapurple.

In addition, there have been some syntactic changes:

  1. rgb() and rgba() functions now accept <number> rather than <integer>.
  2. hsl() and hsla() functions now accept <angle> as well as <number> for hues.
  3. rgb() and rgba(), and hsl() and hsla() are now aliases of each other (all of them have an optional alpha).
  4. rgb(), rgba(), hsl(), and hsla() have all gained a new syntax consisting of space-separated arguments and an optional slash-separated opacity. All the color functions use this syntax form now, in keeping with CSS’s functional-notation design principles.
  5. All uses of <alpha-value> now accept <percentage> as well as <number>.
  6. 4 and 8-digit hex colors have been added, to specify transparency.
  7. The none value has been added, to represent powerless components.

17. Security Considerations

The system colors, if they actually correspond to the user’s system colors, pose a security risk, as they make it easier for a malware site to create user interfaces that appear to be from the system. However, as several system colors are now defined to be "generic", this risk is believed to be mitigated.

18. Privacy Considerations

This specification defines "system" colors, which theoretically can expose details of the user’s OS settings, which is a fingerprinting risk.

19. Accessibility Considerations

This specification encourages authors to not use color alone as a distinguishing feature.

This specification encourages browsers to ensure adequate contrast for specific system color foreground/background pairs. A harder requirement with specific AA or AAA contrast ratios was considered, but since browsers are often just passing along color choices made by the OS, or selected by users (who may have particular requirements, including lower contrast for people living with migraines or epileptic seizures), the CSSWG was unable to require a specific contrast level.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Advisements are normative sections styled to evoke special attention and are set apart from other normative text with <strong class="advisement">, like this: UAs MUST provide an accessible alternative.

Tests

Tests relating to the content of this specification may be documented in “Tests” blocks like this one. Any such block is non-normative.


Conformance classes

Conformance to this specification is defined for three conformance classes:

style sheet
A CSS style sheet.
renderer
A UA that interprets the semantics of a style sheet and renders documents that use them.
authoring tool
A UA that writes a style sheet.

A style sheet is conformant to this specification if all of its statements that use syntax defined in this module are valid according to the generic CSS grammar and the individual grammars of each feature defined in this module.

A renderer is conformant to this specification if, in addition to interpreting the style sheet as defined by the appropriate specifications, it supports all the features defined by this specification by parsing them correctly and rendering the document accordingly. However, the inability of a UA to correctly render a document due to limitations of the device does not make the UA non-conformant. (For example, a UA is not required to render color on a monochrome monitor.)

An authoring tool is conformant to this specification if it writes style sheets that are syntactically correct according to the generic CSS grammar and the individual grammars of each feature in this module, and meet all other conformance requirements of style sheets as described in this module.

Partial implementations

So that authors can exploit the forward-compatible parsing rules to assign fallback values, CSS renderers must treat as invalid (and ignore as appropriate) any at-rules, properties, property values, keywords, and other syntactic constructs for which they have no usable level of support. In particular, user agents must not selectively ignore unsupported component values and honor supported values in a single multi-value property declaration: if any value is considered invalid (as unsupported values must be), CSS requires that the entire declaration be ignored.

Implementations of Unstable and Proprietary Features

To avoid clashes with future stable CSS features, the CSSWG recommends following best practices for the implementation of unstable features and proprietary extensions to CSS.

Non-experimental implementations

Once a specification reaches the Candidate Recommendation stage, non-experimental implementations are possible, and implementors should release an unprefixed implementation of any CR-level feature they can demonstrate to be correctly implemented according to spec.

To establish and maintain the interoperability of CSS across implementations, the CSS Working Group requests that non-experimental CSS renderers submit an implementation report (and, if necessary, the testcases used for that implementation report) to the W3C before releasing an unprefixed implementation of any CSS features. Testcases submitted to W3C are subject to review and correction by the CSS Working Group.

Further information on submitting testcases and implementation reports can be found from on the CSS Working Group’s website at https://www.w3.org/Style/CSS/Test/. Questions should be directed to the public-css-testsuite@w3.org mailing list.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[Bradford-CAT]
Ming R. Luo; R. W. G. Hunt. A Chromatic Adaptation Transform and a Colour Inconstancy Index. Color Research & Application 23(3) 154-158. June 1998.
[CIELAB]
ISO/CIE 11664-4:2019(E): Colorimetry — Part 4: CIE 1976 L*a*b* colour space. 2019. Published. URL: http://cie.co.at/publications/colorimetry-part-4-cie-1976-lab-colour-space-1
[COLORIMETRY]
Colorimetry, Fourth Edition. CIE 015:2018. 2018. URL: http://www.cie.co.at/publications/colorimetry-4th-edition
[Compositing]
Rik Cabanier; Nikos Andronikos. Compositing and Blending Level 1. 13 January 2015. CR. URL: https://www.w3.org/TR/compositing-1/
[CSS-ANIMATIONS-1]
Dean Jackson; et al. CSS Animations Level 1. 11 October 2018. WD. URL: https://www.w3.org/TR/css-animations-1/
[CSS-BACKGROUNDS-3]
Bert Bos; Elika Etemad; Brad Kemper. CSS Backgrounds and Borders Module Level 3. 26 July 2021. CR. URL: https://www.w3.org/TR/css-backgrounds-3/
[CSS-CASCADE-5]
Elika Etemad; Miriam Suzanne; Tab Atkins Jr.. CSS Cascading and Inheritance Level 5. 13 January 2022. CR. URL: https://www.w3.org/TR/css-cascade-5/
[CSS-COLOR-5]
Chris Lilley; et al. CSS Color Module Level 5. 15 December 2021. WD. URL: https://www.w3.org/TR/css-color-5/
[CSS-COLOR-ADJUST-1]
Elika Etemad; et al. CSS Color Adjustment Module Level 1. 10 February 2022. CR. URL: https://www.w3.org/TR/css-color-adjust-1/
[CSS-CONDITIONAL-3]
David Baron; Elika Etemad; Chris Lilley. CSS Conditional Rules Module Level 3. 13 January 2022. CR. URL: https://www.w3.org/TR/css-conditional-3/
[CSS-IMAGES-4]
Tab Atkins Jr.; Elika Etemad; Lea Verou. CSS Image Values and Replaced Content Module Level 4. 13 April 2017. WD. URL: https://www.w3.org/TR/css-images-4/
[CSS-SYNTAX-3]
Tab Atkins Jr.; Simon Sapin. CSS Syntax Module Level 3. 24 December 2021. CR. URL: https://www.w3.org/TR/css-syntax-3/
[CSS-TRANSITIONS-1]
David Baron; et al. CSS Transitions. 11 October 2018. WD. URL: https://www.w3.org/TR/css-transitions-1/
[CSS-VALUES-3]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 3. 6 June 2019. CR. URL: https://www.w3.org/TR/css-values-3/
[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. 16 December 2021. WD. URL: https://www.w3.org/TR/css-values-4/
[CSS2]
Bert Bos; et al. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. 7 June 2011. REC. URL: https://www.w3.org/TR/CSS21/
[CSS3-TEXT-DECOR]
Elika Etemad; Koji Ishii. CSS Text Decoration Module Level 3. 13 August 2019. CR. URL: https://www.w3.org/TR/css-text-decor-3/
[CSSOM-1]
Daniel Glazman; Emilio Cobos Álvarez. CSS Object Model (CSSOM). 26 August 2021. WD. URL: https://www.w3.org/TR/cssom-1/
[Display-P3]
Apple, Inc. Display P3. 2022-02. URL: https://www.color.org/chardata/rgb/DisplayP3.xalter
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FILTER-EFFECTS-1]
Dirk Schulze; Dean Jackson. Filter Effects Module Level 1. 18 December 2018. WD. URL: https://www.w3.org/TR/filter-effects-1/
[HSL]
George H. Joblove, Donald Greenberg. Color spaces for computer graphics. August 1978. URL: https://doi.org/10.1145/965139.807362
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[HWB]
Alvy Ray Smith. HWB — A More Intuitive Hue-Based Color Model. 1996. URL: http://alvyray.com/Papers/CG/HWB_JGTv208.pdf
[ICC]
ICC.1:2010 (Profile version 4.3.0.0). December 2010. URL: http://www.color.org/specification/ICC1v43_2010-12.pdf
[ITU-R-BT.601]
Recommendation ITU-R BT.601. URL: https://www.itu.int/rec/R-REC-BT.601/en
[ITU-R-BT.709]
Recommendation ITU-R BT.709. URL: https://www.itu.int/rec/R-REC-BT.709/en
[MEDIAQUERIES-5]
Dean Jackson; et al. Media Queries Level 5. 18 December 2021. WD. URL: https://www.w3.org/TR/mediaqueries-5/
[OKLab]
Björn Ottosson. A perceptual color space for image processing. December 2020. URL: https://bottosson.github.io/posts/oklab/
[Rec.2020]
Recommendation ITU-R BT.2020-2: Parameter values for ultra-high definition television systems for production and international programme exchange . October 2015. URL: http://www.itu.int/rec/R-REC-BT.2020/en
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[ROMM]
ISO 22028-2:2013 Photography and graphic technology — Extended colour encodings for digital image storage, manipulation and interchange — Part 2: Reference output medium metric RGB colour image encoding (ROMM RGB). 2013-04. URL: https://www.iso.org/standard/56591.html
[SMPTE296]
ST 296:2012, 1280 × 720 Progressive Image 4:2:2 and 4:4:4 Sample Structure — Analog and Digital Representation and Analog Interface. 17 May 2012. Standard. URL: https://doi.org/10.5594/SMPTE.ST296.2012
[SRGB]
Multimedia systems and equipment - Colour measurement and management - Part 2-1: Colour management - Default RGB colour space - sRGB. URL: https://webstore.iec.ch/publication/6169
[SVG11]
Erik Dahlström; et al. Scalable Vector Graphics (SVG) 1.1 (Second Edition). 16 August 2011. REC. URL: https://www.w3.org/TR/SVG11/
[SVG2]
Amelia Bellamy-Royds; et al. Scalable Vector Graphics (SVG) 2. 4 October 2018. CR. URL: https://www.w3.org/TR/SVG2/

Informative References

[DCI-P3]
SMPTE Recommended Practice - D-Cinema Quality — Reference Projector and Environment. 2011. URL: http://ieeexplore.ieee.org/document/7290729/
[JPEG]
Eric Hamilton. JPEG File Interchange Format. September 1992. URL: https://www.w3.org/Graphics/JPEG/jfif3.pdf
[PNG]
Tom Lane. Portable Network Graphics (PNG) Specification (Second Edition). 10 November 2003. REC. URL: https://www.w3.org/TR/PNG/
[ROMM-RGB]
ROMM RGB. URL: https://www.color.org/chardata/rgb/rommrgb.xalter
[Sharma]
G. Sharma; W. Wu; E. N. Dalal. The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations. February 2005. URL: http://www2.ece.rochester.edu/~gsharma/ciede2000/
[TIFF]
TIFF Revision 6.0. 3 June 1992. URL: https://www.loc.gov/preservation/digital/formats/fdd/fdd000022.shtml
[WCAG21]
Andrew Kirkpatrick; et al. Web Content Accessibility Guidelines (WCAG) 2.1. 5 June 2018. REC. URL: https://www.w3.org/TR/WCAG21/
[Wolfe]
Geof Wolfe. Design and Optimization of the ProPhoto RGB Color Encodings. 2011-12-21. URL: http://www.realtimerendering.com/blog/2011-color-and-imaging-conference-part-vi-special-session/

Property Index

Name Value Initial Applies to Inh. %ages Anim­ation type Canonical order Com­puted value
color <color> CanvasText all elements and text yes N/A by computed value type per grammar computed color, see resolving color values
opacity <alpha-value> 1 all elements no map to the range [0,1] by computed value type per grammar specified number, clamped to the range [0,1]

Issues Index

Unsure about when/if a and b become powerless for Lab whites.
Unsure about when/if hue and chroma become powerless for LCH whites.
Computed value needs to be able to represent combinations of currentColor and an actual color. Consider the value of text-emphasis-color in div { text-emphasis: circle; transition: all 2s; }
div:hover { text-emphasis-color: lime; }
em { color: red; }
See Issue 445.