1. Introduction
This section is not normative.
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. 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).
This module adds three functions: color-mix, color-contrast, and a way to modify colors.
The perceptually uniform ``lch()`` colorspace is used for mixing by default, as this has no gamut restrictions and colors are evenly distributed. However, other colorspaces can be specified, including ``hsl()`` or ``srgb`` if desired.
2. Mixing colors: the color-mix function
This function takes two <color> specifications and returns the result of mixing them, in a given colorspace, by a specified amount.
Unless otherwise specified, the mixing is done in the lch() colorspace.
Multiple color functions can be specified.
color-mix() = color-mix( <color> <color> [ <number> | <percentage> | [ <color-function> <colorspace>? ]?] )
mix-color(rgb(0% 42.35% 33.33%), rgb(41.2% 69.88% 96.64%), lightness(40%));
The calculation is as follows:
-
rgb(0% 42.35% 33.33%) is lch(40.083% 32.808 171.175)
-
rgb(41.2% 69.88% 96.64%) is lch(70% 42.5 258.2)
-
mix lightness is 40.083 * 0.4 + 70% * (1 - 0.4) = 58.0332
-
mixed result is lch(58.0332 32.808 171.175)
-
which is a rgb(26.25% 60.68% 50.72%), a lighter green
mix-color(rgb(82.02% 30.21% 35.02%), rgb(5.64% 55.94% 85.31%), hue(75.23%));
The calculation is as follows:
-
rgb(82.02% 30.21% 35.02%) is lch(52% 58.1 22.7)
-
rgb(5.64% 55.94% 85.31%) is lch(56% 49.1 257.1)
-
mix hue is 22.7 * 0.7523 + 257.1 * 0.2477 = 80.76
-
mixed result is lch(52% 58.1 80.76) which is rgb(61.11% 45.85% 0.41%)
mix-color(rgb(82.02% 30.21% 35.02%), rgb(5.64% 55.94% 85.31%), hue(75.23%) lightness(68.4%));
The calculation is as follows:
-
rgb(82.02% 30.21% 35.02%) is lch(52% 58.1 22.7)
-
rgb(5.64% 55.94% 85.31%) is lch(56% 49.1 257.1)
-
mix hue is 22.7 * 0.7523 + 257.1 * 0.2477 = 80.76
-
new lightness is 68.4%
-
mixed result is lch(68.4% 58.1 80.76) which is rgb(79.67% 62.48% 22.09%)
mix-color(red, yellow, lightness(30%));
The calculation is as follows:
-
sRGB red (#F00) is lch(54.2917% 106.8390 40.8526)
-
sRGB yellow (#FF0) is lch(97.6071% 94.7077 99.5746)
-
mix lightness is 54.2917 * 0.3 + 97.6071 * 0.7 = 84.6125
-
mixed result is lch(84.6125% 106.8390 40.8526)
-
which is a very light, saturated red
-
(and well outside the gamut of sRGB: rgb(140.4967% 51.2654% 32.6891%))
-
even outside the gamut of P3: color(display-p3 1.3033 0.5756 0.4003)
-
This example demonstrates that not all colors which can be mixed, can be displayed on current devices.
Instead of a list of color functions, a plain number or percentage can be specified, which applies to all color channels.
Note: interpolating on hue and chroma keeps the intermediate colors as saturated as the endpoint colors.
mix-color(red, yellow, 65%);
The calculation is as follows:
-
sRGB red (#F00) is lch(54.2917% 106.8390 40.8526)
-
sRGB yellow (#FF0) is lch(97.6071% 94.7077 99.5746)
-
mix lightness is 54.2917 * 0.65 + 97.6071 * 0.35 = 69.4521
-
mix chroma is 106.83 * 0.65 + 94.7077 * 0.35 = 102.5872
-
mix hue is 40.8526 * 0.65 + 99.5746 * 0.35 = 61.4053
-
mixed result is lch(69.4521% 102.5872 61.4053)
-
which is a red-orange: rgb(75.3600% 65.6304% 16.9796%)
This shows a desaturated result, compared to LCH interpolation.
mix-color(rgb(82.02% 30.21% 35.02%), rgb(5.64% 55.94% 85.31%), lab() a(38%) b(38%));
The calculation is as follows:
-
rgb(82.02% 30.21% 35.02%) is lab(52% 53.599 22.421)
-
rgb(5.64% 55.94% 85.31%) is lab(56% -10.962 -47.861)
-
a is (53.599 * 0.38) + (-10.962 * 0.62) = 13.572
-
b is (22.421 * 0.38) + (-47.861 * 0.62) = -21.154
-
result is lab(52% 13.572 -21.154) which is rgb(52.446% 45.821% 62.953%)
3. Selecting the most contrasting color: the color-contrast() function
This function takes, firstly, a single color (typically a background, but not necessarily), and then second, a list of two or more colors; it selects from that list the color with highest luminance contrast [WCAG21] to the single color.
color-contrast() = color-contrast( <color> <color># )
color-contrast(wheat tan, sienna, var(--myAccent), #d2691e)
The calculation is as follows:
-
wheat (#f5deb3), the background, has relative luminance 0.749
-
tan (#d2b48c) has relative luminance 0.482 and contrast ratio 1.501
-
sienna (#a0522d) has relative luminance 0.137 and contrast ratio 4.273
Suppose myAccent has the value #b22222:
-
#b22222 has relative luminance 0.107 and contrast ratio 5.081
-
#d2691e has relative luminance 0.305 and contrast ratio 2.249
4. Modifying colors
Note: There are currently two proposals for modifying colors: color-adjust and Relative color syntax.
there are two proposals for color modification (proposal 1, proposal 2). The CSS WG expects that the best aspects of each will be chosen to produce a single eventual solution. <https://github.com/w3c/csswg-drafts/issues/3187>
4.1. Adjusting colors: the color-adjust function
This function takes one <color> specification and returns the result of adjusting that color, in a given colorspace, by a specified transform function.
Unless otherwise specified, the adjustment is done in the lch() colorspace.
Multiple color functions can be specified.
color-adjust() = color-adjust( <color> [ [=color-functions|color-function=] <colorspace>? ]?] )
color-adjust(peru, lightness(-20%));
The calculation is as follows:
-
peru (#CD853F) is lch(62.2532% 54.0114 63.6769)
-
adjusted lightness is 62.2532% - 20% = 42.2532%
-
adjusted result is lch(42.2532% 54.0114 63.6769)
-
which is rgb(57.58%, 32.47%, 3.82%)
4.2. Relative color syntax
Besides specifying absolute coordinates, all color functions can also be used with a *relative syntax* to produce colors in the function’s target color space, based on an existing color (henceforth referred to as "origin color"). This syntax consists of the keyword from, a <color> value, and optionally numerical coordinates specific to the color function. To allow calculations on the original color’s coordinates, there are single-letter keywords for each coordinate and `alpha` that corresponds to the color’s alpha. If no coordinates are specified, the function merely converts the origin color to the target function’s color space.
The following sections outline the relative color syntax for each color function.
A future version of this specification may define a relative syntax for color() as well.
4.2.1. Relative RGB colors
The grammar of the rgb() function is extended as follows:
rgb() = rgb([from <color>]? <percentage>{3} [ / <alpha-value> ]? ) | rgb([from <color>]? <number>{3} [ / <alpha-value> ]? ) <alpha-value> = <number> | <percentage>
When an origin color is present, the following keywords can also be used in this function (provided the end result conforms to the expected type for the parameter) and correspond to:
-
r is a <percentage> that corresponds to the origin color’s red channel after its conversion to sRGB
-
g is a <percentage> that corresponds to the origin color’s green channel after its conversion to sRGB
-
b is a <percentage> that corresponds to the origin color’s blue channel after its conversion to sRGB
-
alpha is a <percentage> that corresponds to the origin color’s alpha transparency
rgb(from indianred, 255 g b)
This takes the sRGB value of indianred (205 92 92) and replaces the red channel with 255 to give rgb(255 92 92).
4.2.2. Relative HSL colors
The grammar of the hsl() function is extended as follows:
hsl() = hsl([from <color>]? <hue> <percentage> <percentage> [ / <alpha-value> ]? ) <hue> = <number> | <angle>
When an origin color is present, the following keywords can also be used in this function (provided the end result conforms to the expected type for the parameter) and correspond to:
-
h is a <number> that corresponds to the origin color’s HSL hue after its conversion to sRGB, normalized to a [0, 360) range.
-
s is a <percentage> that corresponds to the origin color’s HSL saturation after its conversion to sRGB
-
l is a <percentage> that corresponds to the origin color’s HSL lightness after its conversion to sRGB
-
alpha is a <percentage> that corresponds to the origin color’s alpha transparency
--accent: lightseagreen; --complement: hsl(from var(--accent), calc(h+180) s l);
lightseagreen is hsl(177deg 70% 41%), so --complement is hsl(357deg 70% 41%)
4.2.3. Relative HWB colors
The grammar of the hwb() function is extended as follows:
hwb() = hwb([from <color>]? <hue> <percentage> <percentage> [ / <alpha-value> ]? )
When an origin color is present, the following keywords can also be used in this function (provided the end result conforms to the expected type for the parameter) and correspond to:
-
h is a <number> that corresponds to the origin color’s HWB hue after its conversion to sRGB
-
w is a <percentage> that corresponds to the origin color’s HWB whiteness after its conversion to sRGB
-
b is a <percentage> that corresponds to the origin color’s HWB blackness after its conversion to sRGB
-
alpha is a <percentage> that corresponds to the origin color’s alpha transparency
4.2.4. Relative Lab colors
The grammar of the lab() function is extended as follows:
lab() = lab([from <color>]? <percentage> <number> <number> [ / <alpha-value> ]? )
When an origin color is present, the following keywords can also be used in this function (provided the end result conforms to the expected type for the parameter) and correspond to:
-
l is a <percentage> that corresponds to the origin color’s CIE Lightness
-
a is a <number> that corresponds to the origin color’s CIELab a axis
-
b is a <number> that corresponds to the origin color’s CIELab b axis
-
alpha is a <percentage> that corresponds to the origin color’s alpha transparency
-
lab(from var(--mycolor) l a b / 100%) sets the alpha of var(--mycolor) to 100% regardless of what it originally was.
-
lab(from var(--mycolor) l a b / calc(alpha * 0.8)) reduces the alpha of var(--mycolor) by 20% of its original value.
-
lab(from var(--mycolor) l a b / calc(alpha - 20%)) reduces the alpha of var(--mycolor) by 20% of 100%.
Note that all the adjustments are lossless in the sense that no gamut clipping occurs, since lab() encompasses all visible color. This is not true for the alpha adjustments in the sRGB based functions (such as’rgb()', 'hsl()', or 'hwb()'), which would also convert to sRGB in addition to adjusting the alpha transparency.
--mycolor: orchid; // orchid is lab(62.753% 52.460 -34.103) --mygray: lab(from var(--mycolor) l 0 0) // mygray is lab(62.753% 0 0) which is rgb(59.515% 59.515% 59.515%)
4.2.5. Relative LCH colors
The grammar of the lch() function is extended as follows:
lch() = lch([from <color>]? <percentage> <number> <hue> [ / <alpha-value> ]? )
When an origin color is present, the following keywords can also be used in this function (provided the end result conforms to the expected type for the parameter) and correspond to:
-
l is a <percentage> that corresponds to the origin color’s CIE Lightness
-
c is a <number> that corresponds to the origin color’s LCH chroma
-
h is a <number> that corresponds to the origin color’s LCH hue, normalized to a [0, 360) range.
-
alpha is a <percentage> that corresponds to the origin color’s alpha transparency
--mycolor: orchid; // orchid is lch(62.753% 62.571, 326.973) --mygray: lch(from var(--mycolor) l 0 h) // mygray is lch(62.753% 0 326.973) which is rgb(59.515% 59.515% 59.515%)
But now (since the hue was preserved) re-saturating again
--mymuted: lch(from var(--mygray) l 30 h); // mymuted is lch(62.753% 30 326.973) which is rgb(72.710% 53.293% 71.224%)
5. Security and Privacy Considerations
This specification introduces no new security or privacy considerations.
6. Acessibility Considerations
This specification introduces a new feature to help stylesheet authors write stylesheets which conform to WCAG 2.1 section 1.4.3 Contrast (Minimum).