From 191520977b931a87a739384f294c72a1c4dff718 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Tue, 15 Mar 2016 00:07:43 +0100 Subject: [PATCH 1/4] add initial draft of article on color contrast --- source/advanced/color-contrast.html.markdown | 270 +++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 source/advanced/color-contrast.html.markdown diff --git a/source/advanced/color-contrast.html.markdown b/source/advanced/color-contrast.html.markdown new file mode 100644 index 0000000..2a08222 --- /dev/null +++ b/source/advanced/color-contrast.html.markdown @@ -0,0 +1,270 @@ +--- +date: 2016-03-05 21:59:03 +0100 +categories: advanced +author: Tobias Bengfort +summary: Color contrast is essential for legibility. But calculating it correctly is not as simple as it may seem, especially if you consider transparent colors. +--- + +# Color contrast in web design + +Color contrast is essential for legibility in web design. The web content +accessibility guidelines (WCAG) therefore include [strict requirements on +minimum contrast](https://www.w3.org/TR/WCAG20/#visual-audio-contrast). + +In this article I want to explain some details about color contrast and propose +a new algorithm that supports transparency and can be implemented in existing +libraries without loosing backwards compatibility. I will also discuss the +current state of implementation in CSS preprocessors such as +[Less](http://lesscss.org/) or [Sass](http://sass-lang.com/). I personally +prefer Sass, so all examples are written in that language. + +I assume that you have a basic understanding of working with colors, especially +in CSS. + +## Solid colors + +### Definition + +So what is color contrast? There are [many +definitions](https://en.wikipedia.org/wiki/Color_contrast), but the W3C chose +one. As a first step, it decided that there are so many different color +deficiencies that it would not make sense to factor in hue or saturation. So +the contrast formula is purely based on *luma*. + +So what is luma? Especially: What is the difference between luma and the +related concepts *brightness* and *luminance*? + +Brightness is simply the average of the red, green and blue channels. Luma and +luminance on the other hand factor in the fact that humans perceive some colors +as brighter than others. Compare for example yellow and blue. This is done by +using the [following +formula](https://www.w3.org/TR/WCAG20/#relativeluminancedef): + + :::sass + $l = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b + +This formular, as well as everything that follows, assumes that the RGBA values +are in the range from 0 to 1. + +The difference between luminance and luma is that the former is based on raw +RGB values; the latter factors in [*gamma +correction*](https://en.wikipedia.org/wiki/Gamma_correction). + +Humans are better at distinguishing dark colors than bright colors. Gamma +correction is a method to take advantage of that. The gamma correction usually +used on the web is taken from the [sRGB](https://en.wikipedia.org/wiki/Srgb) +standard and can be calculated like this: + + :::sass + @function srgb($channel) { + @if $channel <= 0.03928 { + @return $channel / 12.92; + } @else { + @return pow(($channel + 0.055) / 1.055, 2.4); + } + } + +Putting this together we get this code for calculating the luma: + + :::sass + @function luma($color) { + $r: srgb(red($color) / 255); + $g: srgb(green($color) / 255); + $b: srgb(blue($color) / 255); + @return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b; + } + +Back to contrast: It is now simply defined as a [ratio of +lumas](https://www.w3.org/TR/WCAG20/#contrast-ratiodef): + + :::sass + @function contrast($color1, $color2) { + $l1 = luma($color1); + $l2 = luma($color2); + @return (max($l1, $l2) + 0.05) / (min($l1, $l2) + 0.05); + } + +Note that the contrast can have values between 1 and 21. I am not sure what the +rationale for the 0.05 is. It prevents the formula from going to infinity +for near-black colors, but I find that it still produces overly high results +for those. + +### Implementation + +I have seen three types of functions that you may wish to have in your CSS +preprocessing code: + +- A function that calculates the contrast +- A function that warns you if the contrast is below a certain threshold +- A function that, given a base color, picks the one with the best contrast + out of a list of alternatives. + +Less contains a function to calculate +[luma](http://lesscss.org/functions/#color-channel-luma). It also contains a +function of the third type, called +[`contrast()`](http://lesscss.org/functions/#color-operations-contrast). This +function is not based on the W3C definition of contrast, but there is a [pull +request](https://github.com/less/less.js/pull/2754) to fix that. + +Sass itself does not contain any of it. It even lacks a `pow()` function needed +to calculate gamma correction. Compass (a popular Sass library) has a function +of the third type called +[`contrast-color()`](http://compass-style.org/reference/compass/utilities/color/contrast/) +that is not based on the W3C definition. There is also a more specialised +library called [sass-a11y](https://github.com/at-import/sass-a11y) which +contains a function of the second type called `a11y-contrast()`. This one seems +to be correct. + +## Transparent colors + +So now that we know how to calculate color contrast of solid colors, lets turn +to transparent colors. This topic has been raised (and answered) by [Lea +Verou](http://lea.verou.me/2012/10/easy-color-contrast-ratios/) in 2012. The +standard does not mention transparent colors and most tools are not capable of +working with them, but generally they work the same. + +The additional step you have to do is [*alpha +blending*](https://en.wikipedia.org/wiki/RGBA), i.e. combine the transparent +color with its background color to get the combination: + + :::sass + @function alpha-blend($fg, $bg) { + $a: alpha($fg); + + $r: red($fg) * $a + red($bg) * (1 - $a); + $g: green($fg) * $a + green($bg) * (1 - $a); + $b: blue($fg) * $a + blue($bg) * (1 - $a); + + @return rgb($r, $g, $b); + } + +So we can simply apply alpha blending, then calculate the contrast and we are +done, right? Unfortunately, there are two major issues with this: + +- In order to apply alpha blending, we need to know which color is in the + foreground and which one is in the background. This did not make any + difference before. +- If the background color is transparent itself we need to know (or guess) a + third color (called backdrop) that it will be blended on. + +### Transparent backgrounds + +Let's turn to the issue of the unknown backdrop color first. These are some +approaches I could think of: + +- **Ignore the transparency.** The result of the algorithm should be that + colors with low alpha channel are considered to have a low contrast to + almost everything. So ignoring the transparency completely does not sound + like the right thing to do, though it would save us a lot of headaches. + +- **Use white as a backdrop color as it is the default background color on + most websites.** Using a white background might seem like a good idea, but + using a transparent background on a solid backdrop color is not actually a + common use case and should be advised against (just use the blended color + directly in these cases). [Almost all + examples](http://tympanus.net/codrops/2012/11/26/using-transparency-in-web-design-dos-and-donts/) + I saw used transparent backgrounds to increase contrast when displaying + text over images. So there is really no basis for assuming a white + backdrop. + +- **Calculate the minimum or maximum possible contrast or some combination of + them.** This is what Lea does. It seems to be a bit opinionated for or + against transparency, and there are so many possible ways to combine them + that it is not clear which one to choose. + +- **Calculate the expected value.** This might be the proper approach from a + purely theoretical perspective. It basically means that we choose some + random backdrop colors, calculate the contrast, and use the average of the + results. The big issue with this approach is that we would need to know + how probable each color is as a backdrop. How would we define that? Do a + field study or just assume that all colors are equally probable? This + sounds like overkill. + +Taking into account that the backdrop is most likely not a single color but an +image, I think the most sensible approach is to use the minimum. So how is it +calculated? + +The luma is strictly increasing in relation to every color channel. So the +minimum luma can be achieved by using black as a backdrop color, while the +maximum can be achieved with white. The luma is also continuous, meaning that +for every possible luma between minimum and maximum there is a backdrop color +that can produce it. + +This means that the minimum contrast is 1 if the foreground luma is somewhere +in that range. Otherwise, it is the minimum of the white/black cases: + + :::sass + @function contrast-min($fg, $bg) { + $bg-black: alpha-blend($bg, black); + $bg-white: alpha-blend($bg, white); + + @if luma($bg-white) < luma($fg) { + @return contrast($fg, $bg-white); + } @else if luma($bg-black) > luma($fg) { + @return contrast($fg, $bg-black); + } @else { + @return 1; + } + } + +### Background/foreground + +In the case of solid colors, it was not relevant which of the colors was +background and which was foreground. Now it is: When a transparent foreground +is overlayed on the background, it is mixed with it, resulting in a slightly +decreased contrast. A transparent background can have much more extreme effect +depending on the backdrop. For example, the background may have a lower luma +than the foreground, but when overlayed on white the result has a higher luma. + +This effect is unfortunate, because it makes the API much more complicated and +is largely incompatible with existing implementations. So are there any ways +around it? + +Let's first look at the actual impact. In order to do that we swap foreground +and background colors and compare the results of different algorithms. + +In the case of minimum contrast, the regular and the swapped functions are +correlated (I calculated a correlation of 0.88 in a set of 10000 random +colors). This makes sense because the contrast goes down for both transparent +foreground and transparent background. + +In the case of maximum contrast, they are negatively correlated (-0.23). This +makes sense because the contrast goes down for transparent foreground while it +goes up for transparent background. + +Given the high cost this would have and the high correlation between minimum +contrast and its swapped version, I think it is a sensible approach to use a +"symmetric minimal contrast" that is the average of the two: + + :::sass + @function contrast-min-symmetric($color1, $color2) { + $c1 = contrast-min($color1, $color2); + $c2 = contrast-min($color2, $color1); + @return ($c1 + $c2) / 2; + } + +### Implementation + +- Lea Verou created a [tool](https://leaverou.github.io/contrast-ratio/) + written in JavaScript that reports a range of possible contrasts for + transparent background colors. It requires you to know which of the colors + will be the fore-/background. +- Less ignores the alpha channel completely in its `contrast()` function. +- Compass also ignores the alpha channel in its `contrast-color()` function. +- sass-a11y implements the algorithm proposed by Lea. + +## Conclusion + +This whole topic turned out to be surprisingly complicated, touching topics +such as psycho-visual effects, maintaining backwards compatibility, and a +significant amount of math. + +We discussed ways to ensure a minimum color contrast. Note, however, that this +may not be sufficient to ensure good legibility: Typography and font size are +other key factors. Olso note that too much contrast can be hard on the eyes, +especially very bright colors on dark background. + +While writing this I created several bug reports and pull requests. +Unfortunately, some projects mentioned are no longer maintained, e.g. Compass +and sass-a11y. The symmetric minimal contrast algorithm is now available in a +Sass library called [sass-planifolia](https://github.com/xi/sass-planifolia). From 06d19a6989073d747c7568f6501ab89e26c704e8 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Sun, 17 Apr 2016 11:28:13 +0200 Subject: [PATCH 2/4] incorporate some feedback --- source/advanced/color-contrast.html.markdown | 79 ++++++++++---------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/source/advanced/color-contrast.html.markdown b/source/advanced/color-contrast.html.markdown index 2a08222..78863fd 100644 --- a/source/advanced/color-contrast.html.markdown +++ b/source/advanced/color-contrast.html.markdown @@ -13,13 +13,13 @@ minimum contrast](https://www.w3.org/TR/WCAG20/#visual-audio-contrast). In this article I want to explain some details about color contrast and propose a new algorithm that supports transparency and can be implemented in existing -libraries without loosing backwards compatibility. I will also discuss the +libraries without losing backwards compatibility. I will also discuss the current state of implementation in CSS preprocessors such as -[Less](http://lesscss.org/) or [Sass](http://sass-lang.com/). I personally -prefer Sass, so all examples are written in that language. +[Less](http://lesscss.org/) or [Sass](http://sass-lang.com/). Out of these, I +personally prefer Sass, so all examples are written in that language. -I assume that you have a basic understanding of working with colors, especially -in CSS. +One final note: I assume that you have a basic understanding of working with +colors, especially in CSS. ## Solid colors @@ -27,23 +27,23 @@ in CSS. So what is color contrast? There are [many definitions](https://en.wikipedia.org/wiki/Color_contrast), but the W3C chose -one. As a first step, it decided that there are so many different color -deficiencies that it would not make sense to factor in hue or saturation. So -the contrast formula is purely based on *luma*. +one. As a first step, they decided that contrast should not factor in hue or +saturation in order to make it meaningful for people with different kinds of +color deficiencies. So their contrast formula is purely based on *luma*. -So what is luma? Especially: What is the difference between luma and the +So what is luma? More precisely: What is the difference between luma and the related concepts *brightness* and *luminance*? Brightness is simply the average of the red, green and blue channels. Luma and luminance on the other hand factor in the fact that humans perceive some colors -as brighter than others. Compare for example yellow and blue. This is done by -using the [following +as brighter than others. Compare for example yellow and blue. This can be done +by using the [following formula](https://www.w3.org/TR/WCAG20/#relativeluminancedef): :::sass $l = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b -This formular, as well as everything that follows, assumes that the RGBA values +This formula, as well as everything that follows, assumes that the RGBA values are in the range from 0 to 1. The difference between luminance and luma is that the former is based on raw @@ -51,9 +51,10 @@ RGB values; the latter factors in [*gamma correction*](https://en.wikipedia.org/wiki/Gamma_correction). Humans are better at distinguishing dark colors than bright colors. Gamma -correction is a method to take advantage of that. The gamma correction usually -used on the web is taken from the [sRGB](https://en.wikipedia.org/wiki/Srgb) -standard and can be calculated like this: +correction is a method that takes advantage of that. The gamma correction +usually used on the web is taken from the +[sRGB](https://en.wikipedia.org/wiki/Srgb) standard and can be calculated like +this: :::sass @function srgb($channel) { @@ -64,7 +65,7 @@ standard and can be calculated like this: } } -Putting this together we get this code for calculating the luma: +Putting both formulas together we get this code for calculating the luma: :::sass @function luma($color) { @@ -89,26 +90,26 @@ rationale for the 0.05 is. It prevents the formula from going to infinity for near-black colors, but I find that it still produces overly high results for those. -### Implementation +### Implementations I have seen three types of functions that you may wish to have in your CSS preprocessing code: - A function that calculates the contrast - A function that warns you if the contrast is below a certain threshold -- A function that, given a base color, picks the one with the best contrast - out of a list of alternatives. +- A function that picks the color with the best contrast to a base color out + of a list of alternatives -Less contains a function to calculate -[luma](http://lesscss.org/functions/#color-channel-luma). It also contains a -function of the third type, called +Less contains a [function to calculate +luma](http://lesscss.org/functions/#color-channel-luma). It also contains a +function of the third type called [`contrast()`](http://lesscss.org/functions/#color-operations-contrast). This function is not based on the W3C definition of contrast, but there is a [pull request](https://github.com/less/less.js/pull/2754) to fix that. -Sass itself does not contain any of it. It even lacks a `pow()` function needed -to calculate gamma correction. Compass (a popular Sass library) has a function -of the third type called +Sass, on the other hand, does not contain any of these functions. It even lacks +a `pow()` function needed to calculate gamma correction. However, compass (a +popular Sass library) has a function of the third type called [`contrast-color()`](http://compass-style.org/reference/compass/utilities/color/contrast/) that is not based on the W3C definition. There is also a more specialised library called [sass-a11y](https://github.com/at-import/sass-a11y) which @@ -117,13 +118,14 @@ to be correct. ## Transparent colors -So now that we know how to calculate color contrast of solid colors, lets turn -to transparent colors. This topic has been raised (and answered) by [Lea -Verou](http://lea.verou.me/2012/10/easy-color-contrast-ratios/) in 2012. The -standard does not mention transparent colors and most tools are not capable of -working with them, but generally they work the same. +So now that we know how to calculate color contrast of solid colors, let's turn +to transparent colors. This topic has been raised by [Lea +Verou](http://lea.verou.me/2012/10/easy-color-contrast-ratios/) in 2012. She +also suggested an algorithm that I will discuss in this article. The standard +does not mention transparent colors and most tools are not capable of working +with them, but in theory they work a lot like solid colors. -The additional step you have to do is [*alpha +The one additional step you have to do is [*alpha blending*](https://en.wikipedia.org/wiki/RGBA), i.e. combine the transparent color with its background color to get the combination: @@ -168,9 +170,10 @@ approaches I could think of: backdrop. - **Calculate the minimum or maximum possible contrast or some combination of - them.** This is what Lea does. It seems to be a bit opinionated for or - against transparency, and there are so many possible ways to combine them - that it is not clear which one to choose. + them.** This is what Lea does. The difficulty with this approach is that + using minimum or maximum seems to be a bit biased against or in favour of + transparency. And there are so many possible ways to combine them that it + is not clear which one to choose. - **Calculate the expected value.** This might be the proper approach from a purely theoretical perspective. It basically means that we choose some @@ -181,8 +184,8 @@ approaches I could think of: sounds like overkill. Taking into account that the backdrop is most likely not a single color but an -image, I think the most sensible approach is to use the minimum. So how is it -calculated? +image, I think the most sensible approach out of these is to use the minimum +possible contrast. So how is it calculated? The luma is strictly increasing in relation to every color channel. So the minimum luma can be achieved by using black as a backdrop color, while the @@ -243,7 +246,7 @@ contrast and its swapped version, I think it is a sensible approach to use a @return ($c1 + $c2) / 2; } -### Implementation +### Implementations - Lea Verou created a [tool](https://leaverou.github.io/contrast-ratio/) written in JavaScript that reports a range of possible contrasts for @@ -261,7 +264,7 @@ significant amount of math. We discussed ways to ensure a minimum color contrast. Note, however, that this may not be sufficient to ensure good legibility: Typography and font size are -other key factors. Olso note that too much contrast can be hard on the eyes, +other key factors. Also note that too much contrast can be hard on the eyes, especially very bright colors on dark background. While writing this I created several bug reports and pull requests. From 795551c15c335a3b82d656dde7c1964754e218a1 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Sun, 17 Apr 2016 12:23:45 +0200 Subject: [PATCH 3/4] update less implementation --- source/advanced/color-contrast.html.markdown | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/advanced/color-contrast.html.markdown b/source/advanced/color-contrast.html.markdown index 78863fd..7d067f2 100644 --- a/source/advanced/color-contrast.html.markdown +++ b/source/advanced/color-contrast.html.markdown @@ -252,7 +252,9 @@ contrast and its swapped version, I think it is a sensible approach to use a written in JavaScript that reports a range of possible contrasts for transparent background colors. It requires you to know which of the colors will be the fore-/background. -- Less ignores the alpha channel completely in its `contrast()` function. +- Less ignores the alpha channel completely in its `contrast()` function and + they are [not going to change + that](https://github.com/less/less.js/pull/2843). - Compass also ignores the alpha channel in its `contrast-color()` function. - sass-a11y implements the algorithm proposed by Lea. From a6163eab113b2a6b051abd7e3c8777f9b9078b0c Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Sun, 17 Apr 2016 12:50:30 +0200 Subject: [PATCH 4/4] restructure --- source/advanced/color-contrast.html.markdown | 50 +++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/source/advanced/color-contrast.html.markdown b/source/advanced/color-contrast.html.markdown index 7d067f2..cb14767 100644 --- a/source/advanced/color-contrast.html.markdown +++ b/source/advanced/color-contrast.html.markdown @@ -120,10 +120,9 @@ to be correct. So now that we know how to calculate color contrast of solid colors, let's turn to transparent colors. This topic has been raised by [Lea -Verou](http://lea.verou.me/2012/10/easy-color-contrast-ratios/) in 2012. She -also suggested an algorithm that I will discuss in this article. The standard -does not mention transparent colors and most tools are not capable of working -with them, but in theory they work a lot like solid colors. +Verou](http://lea.verou.me/2012/10/easy-color-contrast-ratios/) in 2012. The +standard does not mention transparent colors, but in theory they work a lot +like solid colors. The one additional step you have to do is [*alpha blending*](https://en.wikipedia.org/wiki/RGBA), i.e. combine the transparent @@ -143,11 +142,20 @@ color with its background color to get the combination: So we can simply apply alpha blending, then calculate the contrast and we are done, right? Unfortunately, there are two major issues with this: +- If the background color is transparent itself we need to know (or guess) a + third color (called backdrop) that it will be blended on. - In order to apply alpha blending, we need to know which color is in the foreground and which one is in the background. This did not make any difference before. -- If the background color is transparent itself we need to know (or guess) a - third color (called backdrop) that it will be blended on. + +Lea got around these issues because she wrote a new library from scratch. Her +function takes foreground and background colors in a specific order and +returns two values: A minimum and a maximum possible contrast resulting from +different backdrops. + +I wanted to come up with an algorithm that could easily be implemented in +existing libraries, so breaking the API was undesirable. So the final sections +of this article will describe the approach I took. ### Transparent backgrounds @@ -170,10 +178,10 @@ approaches I could think of: backdrop. - **Calculate the minimum or maximum possible contrast or some combination of - them.** This is what Lea does. The difficulty with this approach is that - using minimum or maximum seems to be a bit biased against or in favour of - transparency. And there are so many possible ways to combine them that it - is not clear which one to choose. + them.** The difficulty with this approach is that using minimum or maximum + seems to be a bit biased against or in favour of transparency. And there + are so many possible ways to combine them that it is not clear which one to + choose. - **Calculate the expected value.** This might be the proper approach from a purely theoretical perspective. It basically means that we choose some @@ -249,27 +257,33 @@ contrast and its swapped version, I think it is a sensible approach to use a ### Implementations - Lea Verou created a [tool](https://leaverou.github.io/contrast-ratio/) - written in JavaScript that reports a range of possible contrasts for - transparent background colors. It requires you to know which of the colors - will be the fore-/background. + written in JavaScript that reports minimum and maximum possible contrasts + for transparent background colors. It requires you to know which of the + colors will be the fore-/background. - Less ignores the alpha channel completely in its `contrast()` function and they are [not going to change that](https://github.com/less/less.js/pull/2843). - Compass also ignores the alpha channel in its `contrast-color()` function. - sass-a11y implements the algorithm proposed by Lea. +- I created a Sass library called + [sass-planifolia](https://github.com/xi/sass-planifolia) which implements + the symmetric minimal contrast algorithm. ## Conclusion -This whole topic turned out to be surprisingly complicated, touching topics -such as psycho-visual effects, maintaining backwards compatibility, and a -significant amount of math. +This article covered many details about color contrast as well as a new +algorithm that supports transparency and can be implemented in existing +libraries without losing backwards compatibility. We discussed ways to ensure a minimum color contrast. Note, however, that this may not be sufficient to ensure good legibility: Typography and font size are other key factors. Also note that too much contrast can be hard on the eyes, especially very bright colors on dark background. +This whole topic turned out to be surprisingly complicated, touching topics +such as psycho-visual effects, maintaining backwards compatibility, and a +significant amount of math. + While writing this I created several bug reports and pull requests. Unfortunately, some projects mentioned are no longer maintained, e.g. Compass -and sass-a11y. The symmetric minimal contrast algorithm is now available in a -Sass library called [sass-planifolia](https://github.com/xi/sass-planifolia). +and sass-a11y.