1. Introduction
This section is not normative.
This module adds the new functions contrast-color(), color-mix() and light-dark(), and extends existing ones with relative color syntax.
It also extends the color() function so that not only predefined color spaces, but also custom color spaces defined by ICC profiles (including calibrated CMYK) can be used in CSS.
It also adds device-cmyk, a representation of uncalibrated cmyk color.
2. The <color> syntax
Colors in CSS are represented by the <color> type:
<color> = <color-base> | currentColor | <system-color> |
<contrast-color()> | <device-cmyk()> | <light-dark-color>
<color-base> = <hex-color> | <color-function> | <named-color> | <color-mix()> | transparent
<color-function> = <rgb()> | <rgba()> |
<hsl()> | <hsla()> | <hwb()> |
<lab()> | <lch()> | <oklab()> | <oklch()> |
<alpha()> |
<color()>
An absolute color is a <color> whose computed value has an absolute, colorimetric interpretation. This means that the value is not:
-
currentColor (which depends on the value of the color property)
-
a <system-color> (which depends on the color mode)
-
<light-dark-color> (which depends on the color mode)
-
<contrast-color()> (which depends on the color mode)
-
<device-cmyk()> (which has no colorimetric basis)
Nor are any of those values used inside <color-mix()> or in relative color syntax.
The colors that resolve to sRGB are:
The functions that support legacy color syntax are:
The <hsl()>, <hsla()>, <hwb()>, <lch()>, and <oklch()> color functions are cylindrical polar color representations using a <hue> angle; the other color functions use rectangular orthogonal color representations.
3. Mixing Colors: the color-mix() Function
Web developers, design tools and design system developers often use color functions to assist in scaling the design of their component color relations. With the increasing usage of design systems that support multiple platforms and multiple user preferences, like the increased capability of Dark Mode in UI, this becomes even more useful to not need to manually set color, and to instead have a single source from which schemes are calculated.


Currently Sass, calc() on HSL values, or PostCSS is used to do this. However, preprocessors are unable to work on dynamically adjusted colors; all current solutions are restricted to the sRGB gamut and to the perceptual limitations of HSL (colors are bunched up in the color wheel, and two colors with visually different lightness, like yellow and blue, can have the same HSL lightness).
To meet this need, the color-mix() function takes a list of one or more <color> specifications and returns the result of mixing them, in a given <color-space>, in the specified amounts.
color-mix() = color-mix( <color-interpolation-method>? , [ <color> && <percentage [0,100]>? ]#)
Tests
3.1. Colorspace for mixing
If no color interpolation method is specified, assume Oklab. Otherwise, use the specified colorspace for mixing.
color-mix ( in oklab, firebrick, goldenrod) color-mix ( firebrick, goldenrod)
3.2. Percentage Normalization
Percentages are required to be in the range 0% to 100%. Negative percentages are specifically disallowed. Percentages are normalized by normalizing mix percentages.
Tests
color-mix ( in lch, purple50 % , plum50 % ) color-mix ( in lch, purple50 % , plum) color-mix ( in lch, purple, plum50 % ) color-mix ( in lch, purple, plum) color-mix ( in lch, plum, purple) color-mix ( in lch, purple80 % , plum80 % )
All produce a 50-50 mix of purple and plum, in lch: lch(51.51% 52.21 325.8) which is rgb(68.51% 36.01% 68.29%).
However, this form is not the same, as the alpha is less than one:
color-mix ( in lch, purple30 % , plum30 % )
This produces lch(51.51% 52.21 325.8 / 0.6) which is rgb(68.51% 36.01% 68.29% / 0.6).
3.3. Calculating the Result of color-mix
-
Normalize mix percentages from the list of mix items passed to the function, with the "forced normalization" flag set to true, letting items and leftover be the result.
-
Let alpha mult be
1 - leftover, interpreting leftover as a number between 0 and 1. -
If items is length 1, set color to the color of that sole item, converted to the specified interpolation <color-space>.
Otherwise:
-
Let item stack be a stack made by reversing items. (Thus, with the first item at the top of the stack.)
-
While item stack has length 2 or greater:
-
Pop from item stack twice, letting a and b be the two results in order. Let combined percentage be the sum of a and b’s percentages.
-
Interpolate a and b’s colors as described in CSS Color 4 § 13. Color Interpolation, with a progress percentage equal to
(b’s percentage) / combined percentage), if combined percentage is greater than 0, and 0.5 otherwise. If the specified color space is a cylindrical polar color space, then the <hue-interpolation-method> controls the interpolation of hue, as described in CSS Color 4 § 13.4 Hue Interpolation. If no <hue-interpolation-method> is specified, assume shorter. -
Create a new mix item with the resulting color and a percentage of combined percentage, and push it onto item stack.
-
-
Set color to the color of the sole remaining item in item stack.
-
-
Multiply the alpha component of color by alpha mult.
-
Return color.
Note: In cylindrical polar color spaces, mixing is order-dependent, as which direction is “shorter” or “longer” around the hue circle can change depending on what other mixes have already been performed. This algorithm mixes each color in the specified order, mixing the result with the next in the list. For rectangular orthogonal color spaces, the order doesn’t matter, and the process can be simplified.
Tests
- color-mix-basic-001.html (live test) (source)
- color-mix-missing-components.html (live test) (source)
- color-mix-non-srgb-001.html (live test) (source)
- color-computed-color-mix-function.html (live test) (source)
- color-invalid-color-mix-function.html (live test) (source)
- color-valid-color-mix-function.html (live test) (source)
- color-mix-out-of-gamut.html (live test) (source)
- 2d.fillStyle.colormix.html (live test) (source)
- 2d.fillStyle.colormix.currentcolor.html (live test) (source)
- 2d.strokeStyle.colormix.html (live test) (source)
color-mix ( in lch, peru40 % , palegoldenrod)
The mixing is done in lch color space. Here is a top-down view, looking along the neutral L axis:
The calculation is as follows:
-
peru is lch(62.253% 54.011 63.677)
-
palegoldenrod is lch(91.374% 31.406 98.834)
-
the mixed lightness is 62.253 * 40/100 + 91.374 * (100-40)/100 = 79.7256
-
the mixed chroma is 54.011 * 40/100 + 31.406 * (100-40)/100 = 40.448
-
the mixed hue is 63.677 * 40/100 + 98.834 * (100-40)/100 = 84.771
-
the mixed result is lch(79.7256% 40.448 84.771)
Note: interpolating on hue and chroma keeps the intermediate colors as saturated as the endpoint colors.
color-mix ( in lch, teal65 % , olive);
The calculation is as follows:
-
sRGB teal (#008080) is lch(47.9855% 31.6903 196.4524)
-
sRGB olive (#808000) is lch(52.1496% 56.8124 99.5746)
-
mixed lightness is 47.9855 * 0.65 + 52.1496 * 0.35 = 49.4429
-
mixed chroma is 31.6903 * 0.65 + 56.8124 * 0.35 = 40.4830
-
mixed hue is 196.4524 * 0.65 + 99.5746 * 0.35 = 162.5452
-
mixed result is lch(49.4429% 40.4830 162.5452)
-
which is a slightly-blueish green: rgb(7.7377% 52.5730% 37.3213%)
color-mix ( in oklch, teal0 % , olive0 % );
Thus, the result is transparent black, in the oklch color space:
oklch(0% 0 none / 0)
color-mix ( in oklab, teal, olive, blue);
The calculation is as follows:
-
teal (#008080) is oklab(54.31% -0.0896 -0.0236)
-
olive (#808000) is oklab(58.07% -0.0428 0.1191)
-
blue (#0000FF) is oklab(45.20% -0.0325 -0.3115)
-
mixed lightness is (54.31 + 58.07 + 45.20) / 3 = 52.53%
-
mixed a is (-0.0896 + -0.0428 + -0.0325) / 3 = -0.0550
-
mixed b is (-0.0236 + 0.1191 + -0.3115) / 3 = -0.0720
-
mixed result is oklab(52.53% -0.0550 -0.0720)
3.4. Effect of Mixing Color Space on color-mix
The choice of mixing color space can have a large effect on the end result.
color-mix ( in lch, white, black); color-mix ( in xyz, white, black); color-mix ( in srgb, white, black);
The calculation is as follows:
-
sRGB white (#FFF) is lch(100% 0 0)
-
sRGB black (#000) is lch(0% 0 0)
-
The mix in LCH is lch(50% 0 0)
-
The mix in XYZ is lch(76% 0 0)
-
The mix in sRGB is lch(53.4% 0 0)
The mix in LCH gives an L value of 50%, a perfect mid gray, exactly as expected (mixing in Lab would do the same, as the Lightness axis is the same in LCH and Lab).
The mix in XYZ gives a result that is too light; XYZ is linear-light but is not perceptually uniform. The mix in sRGB gives a result that is a bit too light; sRGB is neither perceptually uniform nor linear-light.
color-mix ( in xyz, rgb ( 82.02 % 30.21 % 35.02 % ) 75.23 % , rgb ( 5.64 % 55.94 % 85.31 % ));
The calculation is as follows:
-
rgb(82.02% 30.21% 35.02%) is lch(52% 58.1 22.7) which is X=0.3214, Y=0.2014, Z=0.0879.
-
rgb(5.64% 55.94% 85.31%) is lch(56% 49.1 257.1) which is X=0.2070, Y=0.2391, Z=0.5249.
-
mixed result X=(0.3214 * 0.7523) + (0.2070 * (1 - 0.7523)) = 0.29306.
-
mixed result Y=(0.2014 * 0.7523) + (0.2391 * (1 - 0.7523)) = 0.21074.
-
mixed result Z=(0.0879 * 0.7523) + (0.5249 * (1 - 0.7523)) = 0.19614.
-
mix result is lch(53.0304% 38.9346 352.8138) which is rgb(72.300% 38.639% 53.557%)
This example is a 50% mix of white and blue, in three different color spaces.
color-mix ( in lch, white, blue); color-mix ( in oklch, white, blue); color-mix ( in srgb, white, blue);
The calcuation is as follows:
-
white is rgb(100% 100% 100%) which is lch(100% 0 none) which is oklch(100% 0 none)
-
blue is rgb(0% 0% 100%) which is lch(29.5683% 131.201 301.364) which is oklch(45.201% 0.31321 264.052)
-
mix in lch is lch(64.7841% 65.6008 301.364) which is quite purple
-
mix in oklch is oklch(72.601% 0.15661 264.052)
-
mix in srgb is rgb(50% 50% 100%) which is also a bit purple
color-mix ( in hsl, color ( display-p30 1 0 ) 80 % , yellow);
The calcuation is as follows:
-
color(display-p3 0 1 0) is color(srgb -0.5116 1.01827 -0.3107) which is outside the sRGB gamut
-
Converted to hsl hsl(127.879 301.946 25.334)
-
yellow is hsl(60 100% 50%)
-
the hue is 127.879 × 0.8 + 60 × 0.2 = 114.3032
-
the saturation is 301.946 × 0.8 + 100 × 0.2 = 261.5568
-
the lightness is 25.334 × 0.8 + 50 × 0.2 = 30.2672
-
the mixed result is hsl(114.3032 261.5568 30.2672) which is color(srgb -0.3387 1.0943 -0.48899)
color-mix ( in lab, device-cmyk ( 0.091777 0.043303 0.312816 0.000000 ) 100 % , yellow);
Since the first color is at 100%, the second color is 0% and does not affect the mixed result in any way. The result is thus the computed value of the first color, in CIE Lab.
To visualize the result, let us say that the device CMYK values are in fact to be printed using SWOP 2006 coated.
-
device-cmyk(0.091777 0.043303 0.312816 0.000000) is lab(91.44% 4.142 20.52)
Suppose the implementation uses an ICC profile to obtain lab() colors, and in this example a FOGRA39 Coated profile is used:
-
device-cmyk(0.091777 0.043303 0.312816 0.000000) is lab(91.840596 -3.559090 20.449159)
-
The deltaE 2000 between this and the original printed color is 8.17 which is clearly visible.
Now suppose another implementation uses the naive color conversion algorithm, giving an sRGB result.
-
device-cmyk(0.091777 0.043303 0.312816 0.000000) is rgb(90.8223% 95.6697% 68.7184%) which is lab(94.02% -12.31 31.79)
-
The deltaE 2000 between this and the original printed color is 14.3 which is very visible.
3.5. Effect of Non-Unity Alpha on color-mix
So far, all the color-mix() examples have used fully opaque colors. To simplify the examples, the premultilication and unpremultiplication steps were omitted because these would simply multiply by 1, and divide by 1, so the result would be unchanged.
In the general case, colors may have non-unity alpha components and thus the premultiply, interpolate, unpremultiply steps must not be omitted.
color-mix ( in srgb, rgb ( 100 % 0 % 0 % /0.7 ) 25 % , rgb ( 0 % 100 % 0 % /0.2 ));
The calcuation is as follows:
-
rgb(100% 0% 0% / 0.7) when premultiplied, is [0.7, 0, 0]
-
rgb(0% 100% 0% / 0.2) when premultiplied, is [0, 0.2, 0]
-
the premultiplied, interpolated result is [0.7 * 0.25 + 0 * (1 - 0.25), 0 * 0.25 + 0.2 * (1 - 0.25), 0 * 0.25 + 0 * (1 - 0.25)] which is [0.175, 0.150, 0]
-
the interpolated alpha is 0.7 * 0.25 + 0.2 * (1 - 0.25) = 0.325
-
the un-premultiplied result is [0.175 / 0.325, 0.150 / 0.325, 0 / 0.325] which is [0.53846, 0.46154, 0]
-
so the mixed color is color(srgb 0.53846 0.46154 0 / 0.325)
The incorrect calculation would be:
-
the interpolated result is [1 * 0.25 + 0 * (1 - 0.25), 0 * 0.25 + 1 * (1 - 0.25), 0 * 0.25 + 0 * (1 - 0.25)] which is [0.25, 0.75, 0]
-
so the incorrect mixed color is color(srgb 0.25 0.75 0 / 0.325)
This is a huge difference; the ΔE2000 between the correct and incorrect results is 30.7!
When the percentage normalization generates an alpha multiplier, the calculation is the same except for an additional last step.
However in this case the percentages are specified as 20% of the first color and 60% of the second. This adds to 80% so the alpha multiplier is 0.8.
The mix percentages are then scaled
by a factor of 100/80:
20% * 100/80 = 25%
60% * 100/80 = 75%
giving the same final mix percentages as the previous example.
color-mix ( in srgb, rgb ( 100 % 0 % 0 % /0.7 ) 20 % , rgb ( 0 % 100 % 0 % /0.2 ) 60 % );
The calcuation is as follows:
-
rgb(100% 0% 0% / 0.7) when premultiplied, is [0.7, 0, 0]
-
rgb(0% 100% 0% / 0.2) when premultiplied, is [0, 0.2, 0]
-
the premultiplied, interpolated result is [0.7 * 0.25 + 0 * (1 - 0.25), 0 * 0.25 + 0.2 * (1 - 0.25), 0 * 0.25 + 0 * (1 - 0.25)] which is [0.175, 0.150, 0]
-
the interpolated alpha is 0.7 * 0.25 + 0.2 * (1 - 0.25) = 0.325
-
the un-premultiplied result is [0.175 / 0.325, 0.150 / 0.325, 0 / 0.325] which is [0.53846, 0.46154, 0]
-
so the mixed color would be color(srgb 0.53846 0.46154 0 / 0.325)
-
there is a 0.8 alpha multiplier, so the alpha of the mixed result is actually 0.325 * 0.8 = 0.260 so the mixed color is actually color(srgb 0.53846 0.46154 0 / 0.260)
Note: do not multiply the interpolated alpha by the alpha multiplier and then use that to undo premultiplication. That would be correct if the mix percentages were not scaled to sum to 100%, but they are, so doing it this way would adjust the mixed color twice.
4. Relative Colors
4.1. Processing Model for Relative Colors
In previous levels of this specification, the color functions could only specify colors in an absolute manner, by directly specifying all of the color components.
The new relative color syntax extends modern color syntax to allow existing colors to be modified using the color functions: if an origin color is specified, then each color component (and the alpha component, if specified) can either be directly specified, or taken from the origin color (and possibly modified with math functions).
Most relative colors allow modification of both the color components and the alpha component. However, there is a simpler and more restricted form in which only the alpha component is modified.
--base: gold; --softerbase: oklch(from var(--base) l calc(c * 0.9) h);
While here we change just the opacity:
--marker: teal; --palemarker: alpha(from var(--marker) / 0.7);
The origin color and the relative color need not use the same color function. Thus, we define a relative color processing space, which is the color space within which computation of color values takes place; this also affects the serialization of the result of the relative color.
-
For the alpha-only form, the relative color processing space is that of the origin color.
-
For the color-affecting forms, the relative color processing space is that of the relative color function.
Conversion, if required: All operations take part in the relative color processing space. If the originally specified color space for the origin color used a different color function, it’s first converted into the relative color processing space, so it has meaningful values for the components, and color component keywords refer to that color space.
If the alpha value of the relative color is omitted, it defaults to that of the origin color (rather than defaulting to 100%, as it does in the absolute syntax).
When relative color syntax is used, color component values, whether directly specified or arising from color space conversion, are not clamped to the reference ranges but are retained as-is. This preserves out of gamut values, if the destination color space is capable of representing them.
However, when relative color syntax is used, alpha component values whether directly specified or arising from color space conversion, are clamped to the reference range.
Missing components are handled the same way as with CSS Color 4 § 13.2 Interpolating with Missing Components: the origin colorspace and the relative function colorspace are checked for analogous components which are then carried forward as missing.
While most uses of relative color syntax will use the component keywords in their corresponding argument, you can use them in any position.
Beware when using components outside their normal position; when percentages are resolved to numbers, there is no "magic scaling" to account for the changed position if those numbers are used in a different place.
There is no relative device-cmyk() syntax.
4.2. Relative Color Syntax
The precise details of each function’s syntactic changes to accommodate relative colors are listed below, but they all follow a common structure:
-
An origin color can be specified with a from <color> value at the start of the function. This includes the optional alpha component, if specified.
-
If no origin color is specified, the function is not a relative color.
-
If an origin color is specified, the remaining arguments can either be specified directly, as normal, or be specified as a component keyword referring to one of the components of the origin color converted to the relative color processing space. Math functions can also use these keywords to do dynamic modifications of the origin color’s components.
-
Relative color syntax does