Skip to content

Commit

Permalink
[css-color-6] contrast-color() algorithm syntax changes
Browse files Browse the repository at this point in the history
- If color candidates are provided, algorithm is mandatory, otherwise a UA dependent algorithm is used (part of the resolution in #7361 )
- Reorganization of prose and examples
- Add keyword to specify algorithm without target contrast

Co-Authored-By: fantasai <[email protected]>
  • Loading branch information
LeaVerou and fantasai committed Oct 20, 2022
1 parent 9065544 commit e59859d
Showing 1 changed file with 127 additions and 108 deletions.
235 changes: 127 additions & 108 deletions css-color-6/Overview.bs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Introduction {#intro}

<em>This section is not normative.</em>

This module adds two one function:
This module adds a new <<color>> function:
''contrast-color()''.


Expand All @@ -52,88 +52,44 @@ Introduction {#intro}
https://caniuse.com/mdn-css_types_color_color-contrast
-->

Computing a sufficiently contrasting color: the ''contrast-color()'' function {#colorcontrast}
Computing a Contrasting Color: the ''contrast-color()'' function {#colorcontrast}
========================================================================================

The purpose of this function is to generate sufficiently contrasting colors from other colors,
without authors having to compute them manually.
The <dfn>contrast-color()</dfn> [=functional notation=]
identifies a sufficiently contrasting color
against a specified background or foreground color
without requiring manual computation.

Its syntax is:

<pre class='prod'>
<dfn>contrast-color()</dfn> = contrast-color( <<color>> [ vs <<color>>#{2,} ]? [ to <<target-contrast>>]? )
<dfn><<target-contrast>></dfn> = wcag2([<<number>> | [ aa | aaa ] && large? ])
<<contrast-color()>> = contrast-color( <<color>> [[ vs <<color>># ]? to <<target-contrast>>]? )
<<target-contrast>> = <<wcag2>>
</pre>

The only mandatory argument is the first color, which is the color to be contrasted,
typically (but not necessarily) a background color.
The only mandatory argument is the <dfn lt="contrast base color" local-lt="base color">base color</dfn>
against which the contrast is computed.
This is typically (but not necessarily) a background color.

An optional list of color candidates is provided after a ''vs'' keyword.
An optional list of <dfn local-lt="color candidates">contrast color candidates</dfn> may be provided after a ''vs'' keyword.
If no candidates are provided, the default candidates are used: ''white, black''.

An optional target <a href="https://www.w3.org/TR/WCAG21/#contrast-minimum">luminance contrast</a> [[!WCAG21]]
is provided after a ''contrast-color()/to'' keyword.
The <dfn><<target-contrast>></dfn> argument specifies the contrast algorithm to use.
If no [=color candidates=] have been provided,
<<target-contrast>> may be omitted,
in which case a UA-chosen algorithm is used.
Arguments to a <<target-contrast>> [=functional notation=]
indicate the <dfn>target contrast level</dfn>.

If a target contrast is specified,
the <a href="#winner">returned color</a> is the first color candidate that meets or exceeds the target contrast.
If no target is given,
If the [=target contrast level=] is omitted,
the color candidate with the greatest contrast is returned.
If multiple candidates have the same contrast,
the one that comes earliest in the list is returned.
Otherwise,
the returned color is the first color candidate that meets or exceeds that level,
defaulting to ''white'' or ''black'' if none qualify.

The single color is separated from the list
with the keyword <css>vs</css>
and the target contrast, if present, is separated from the list
with the keyword <css>to</css>.

The target contrast is specified by providing a function for the algorithm used
(currently only [WCAG 2.1](#luminance-contrast) is supported )
with the contrast ratio as the argument.

For ''wcag2()'', ''aa'' is equivalent to ''4.5'',
''aa large'' (or ''large aa'') is equivalent to ''3'',
''aaa'' is equivalent to ''7'',
and ''aaa large'' (or ''large aaa'') is equivalent to ''4.5''.

<h3 id="luminance-contrast">
Calculating luminance and WCAG 2.1 contrast
</h3>

For each color in the list,
the CIE Luminance (Y) is calculated,
relative to a [=D65 whitepoint=].

For each pair of colors,
the WCAG 2.1 contrast is calculated:
contrast = (Y<sub>l</sub> + 0.05) / (Y<sub>d</sub> + 0.05)
where Y<sub>d</sub> is the luminance of the darker color in the pair
and Y<sub>l</sub> is the luminance of the lighter color.
The factor 0.05 represents the luminance contribution of the viewing flare.

<div class="example">
Suppose that the single color was

<pre class="lang-css">color(display-p3 0.38 0.11 0.05)</pre>

while the first color in the list was

<pre class="lang-css">yellow</pre>

The calculation is as follows:
* <span class="swatch" style="--color: rgb(41.482% 7.941% 1.375%)"></span> color(display-p3 0.38 0.11 0.05) is <span class="swatch" style="--color: rgb(41.482% 7.941% 1.375%)"></span> color(xyz 0.06191 0.03568 0.00463) so the relative luminance is <b>0.03568</b>
* <span class="swatch" style="--color: yellow"></span> yellow is <span class="swatch" style="--color: yellow"></span> rgb(100% 100% 0%) which is <span class="swatch" style="--color: yellow"></span> color(xyz 0.76998 0.92781 0.13853) so the relative luminance is <b>0.92781</b>
* the contrast is (0.92781 + 0.05) / (0.03568 + 0.05) = <b>11.4123</b>
</div>

<h3 id="winner">
Finding the winning color
</h3>

It then selects from that list
the first color to meet or exceed the target contrast.
If no target is specified,
it selects the first color with the highest contrast
to the single color.
<h3 id="contrast-color-winner">
Finding the Winning Color</h3>

<!--
<wpt>
Expand All @@ -144,20 +100,14 @@ Computing a sufficiently contrasting color: the ''contrast-color()'' function {#
</wpt>
-->

<div class="example">
<pre class="lang-css">contrast-color(wheat vs tan, sienna, var(--myAccent), #d2691e)</pre>

The calculation is as follows:
* <span class="swatch" style="--color: wheat"></span> wheat (#f5deb3), the background, has relative luminance 0.749
* <span class="swatch" style="--color: tan"></span> tan (#d2b48c) has relative luminance 0.482 and contrast ratio <strong>1.501</strong>
* <span class="swatch" style="--color: sienna"></span> sienna (#a0522d) has relative luminance 0.137 and contrast ratio <strong>4.273</strong>

Suppose myAccent has the value <span class="swatch" style="--color: #b22222"></span> #b22222:
* #b22222 has relative luminance 0.107 and contrast ratio <strong>5.081</strong>
* <span class="swatch" style="--color: #d2691e"></span> #d2691e has relative luminance 0.305 and contrast ratio <strong>2.249</strong>
The highest contrast ratio is <strong>5.081</strong> so var(--myAccent) wins
<h4 id="contrast-color-target-winner">
If there is a target contrast</h4>

</div>
Candidate colors are tested sequentially,
starting with the first color in the list,
and ending with an automatically appended ''white, black''.
The first color to pass the specified level of contrast
against the [=base color=] wins.

<!-- live example
https://colorjs.io/notebook/?storage=https%3A%2F%2Fgist.github.com%2Fsvgeesus%2Fec249f376fcecbaa8794f75dbfc1dacf
Expand All @@ -169,14 +119,12 @@ Computing a sufficiently contrasting color: the ''contrast-color()'' function {#
* <span class="swatch" style="--color: wheat"></span> wheat (#f5deb3), the background, has relative luminance 0.749
* <span class="swatch" style="--color: bisque"></span> bisque (#ffe4c4) has relative luminance 0.807 and contrast ratio <strong>1.073</strong>
* <span class="swatch" style="--color: darkgoldenrod"></span> darkgoldenrod (#b8860b) has relative luminance 0.273 and contrast ratio <strong>2.477</strong>
* <span class="swatch" style="--color: olive"></span> olive (#808000 ) has relative luminance 0.200 and contrast ratio <strong>3.193</strong>
* <span class="swatch" style="--color: olive"></span> olive (#808000) has relative luminance 0.200 and contrast ratio <strong>3.193</strong>
* <span class="swatch" style="--color: sienna"></span> sienna (#a0522d) has relative luminance 0.137 and contrast ratio <strong>4.274</strong>
* <span class="swatch" style="--color: darkgreen"></span> darkgreen (#006400 ) has relative luminance 0.091 and contrast ratio <strong>5.662</strong>
* <span class="swatch" style="--color: maroon"></span> maroon (#800000 ) has relative luminance 0.046 and contrast ratio <strong>8.333</strong>

* <span class="swatch" style="--color: maroon"></span> maroon (#800000) has relative luminance 0.046 and contrast ratio <strong>8.333</strong>

The first color in the list which meets the desired contrast ratio of 4.5 is <span class="swatch" style="--color: darkgreen"></span> darkgreen.

</div>

<div class="example">
Expand All @@ -186,47 +134,56 @@ Computing a sufficiently contrasting color: the ''contrast-color()'' function {#
* the relative luminances and contrast ratios are the same as the previous example.

The first color in the list which meets the desired contrast ratio of 5.8 is <span class="swatch" style="--color: maroon"></span> maroon.
</div>

<div class="example">
<pre class="lang-css">contrast-color(wheat vs bisque, darkgoldenrod, olive to wcag2(AA))</pre>

The calculation is as follows:
* the relative luminances and contrast ratios are the same as the previous example.

No color in the list meets the desired contrast ratio of 4.5.
The contrast of <span class="swatch" style="--color: white"></span> ''white'' against the
[=base color=] of <span class="swatch" style="--color: wheat"></span> ''wheat'' is 1.314, which is smaller than 4.5 (AA),
and thus does not pass the target contrast.
Therefore, <span class="swatch" style="--color: black"></span> ''black'' is returned, which has a contrast of 15.982 > 4.5.
</div>

The candidate colors are tested sequentially,
from left to right;
a color is the temporary winner
if it has the highest contrast of all those tested so far.
<h4 id="contrast-color-max-winner">
If no target contrast is specified</h4>

List traversal is terminated once the target contrast has been met or exceeded.
Candidate colors are tested sequentially,
starting with the first color in the list.
A color is the <var>temporary winner</var>
if it has the highest contrast against the [=base color=] of all those tested so far.

Once the end of the list is reached, if there is no target contrast,
the current temporary winner is the overall winner.
Once the end of the list is reached, the current <var>temporary winner</var> is the overall winner.
Thus, if two colors in the list happen to have the same contrast,
the earlier in the list wins
because the later one has the same contrast, not higher.

If there is a target contrast,
and the end of the list is reached without meeting that target,
either ''white'' or ''black'' is returned,
whichever has the higher contrast.
the earlier one wins.

<div class="example">
<pre class="lang-css">contrast-color(wheat vs bisque, darkgoldenrod, olive to wcag2(AA))</pre>
<pre class="lang-css">
--myAccent: #b22222;
color: contrast-color(wheat vs tan, sienna, var(--myAccent), #d2691e to wcag2)
</pre>

The calculation is as follows:
* the relative luminances and contrast ratios are the same as the previous example.

No color in the list meets the desired contrast ratio of 4.5,
so <span class="swatch" style="--color: black"></span> black
is returned as the contrast (15.982)
is higher than that of white (1.314).
* <span class="swatch" style="--color: wheat"></span> wheat (#f5deb3), the background, has relative luminance 0.749
* <span class="swatch" style="--color: tan"></span> tan (#d2b48c) has relative luminance 0.482 and contrast ratio <strong>1.501</strong>
* <span class="swatch" style="--color: sienna"></span> sienna (#a0522d) has relative luminance 0.137 and contrast ratio <strong>4.273</strong>
* <span class="swatch" style="--color: #b22222"></span> --myAccent (#b22222) has relative luminance 0.107 and contrast ratio <strong>5.081</strong>
* <span class="swatch" style="--color: #d2691e"></span> #d2691e has relative luminance 0.305 and contrast ratio <strong>2.249</strong>

The highest contrast ratio is <strong>5.081</strong> so ''var(--myAccent)'' wins
</div>

<div class="example">
<pre class="lang-css">
foo {
--bg: hsl(200 50% 80%);
--purple-in-hsl: hsl(300 100% 25%);
color: contrast-color(var(--bg) vs hsl(200 83% 23%), purple, var(--purple-in-hsl));
}
color: contrast-color(var(--bg) vs hsl(200 83% 23%), purple, var(--purple-in-hsl) to wcag2);
}
</pre>

The calculation is as follows:
Expand All @@ -241,6 +198,68 @@ Computing a sufficiently contrasting color: the ''contrast-color()'' function {#
<!-- great example to use in WPT -->
</div>

<h3 id="contrast-algorithms">
Contrast algorithms
</h3>

ISSUE: Currently only [[#luminance-contrast|WCAG 2.1]] is supported,
however this algorithm is known to have problems,
particularly on dark backgrounds.
Future revisions of this module will likely introduce additional contrast algorithms.

<h4 id="luminance-contrast">
WCAG 2.1: the ''wcag2'' keyword and ''wcag2()'' function
</h4>

The <dfn for="contrast-color()" value>wcag2</dfn> keyword and <dfn for="contrast-color()">wcag2()</dfn> [=functional notations=]
indicate use of the [[!WCAG21]] <a href="https://www.w3.org/TR/WCAG21/#contrast-minimum">luminance contrast</a> algorithm.
Their syntax is:

<pre class="prod">
<dfn><<wcag2>></dfn> = wcag | wcag2([<<number>> | [ aa | aaa ] && large? ])
</pre>

Its [=target contrast level=] keywords map as follows:
* ''aa'' computes to ''4.5''
* ''aa large'' (or ''large aa'') computes to ''3''
* ''aaa'' computes to ''7''
* ''aaa large'' (or ''large aaa'') computes to ''4.5''

To find the contrast of a pair of colors,
first their CIE Luminance (Y) is calculated relative to a [=D65 whitepoint=].
Then the WCAG 2.1 contrast is calculated:
contrast = (Y<sub>l</sub> + 0.05) / (Y<sub>d</sub> + 0.05)
where Y<sub>d</sub> is the luminance of the darker color in the pair
and Y<sub>l</sub> is the luminance of the lighter color.
The factor 0.05 represents the luminance contribution of the viewing flare.

<div class="example">
Suppose that the [=base color=] were

<pre class="lang-css">color(display-p3 0.38 0.11 0.05)</pre>

while the first [=candidate color=] in the list were

<pre class="lang-css">yellow</pre>

The calculation is as follows:
* <span class="swatch" style="--color: rgb(41.482% 7.941% 1.375%)"></span> color(display-p3 0.38 0.11 0.05) is <span class="swatch" style="--color: rgb(41.482% 7.941% 1.375%)"></span> color(xyz 0.06191 0.03568 0.00463) so the relative luminance is <b>0.03568</b>
* <span class="swatch" style="--color: yellow"></span> yellow is <span class="swatch" style="--color: yellow"></span> rgb(100% 100% 0%) which is <span class="swatch" style="--color: yellow"></span> color(xyz 0.76998 0.92781 0.13853) so the relative luminance is <b>0.92781</b>
* the contrast is (0.92781 + 0.05) / (0.03568 + 0.05) = <b>11.4123</b>
</div>














<!--
████████ ████████ ██████ ███████ ██ ██ ██ ████████
Expand Down

0 comments on commit e59859d

Please sign in to comment.