The intent of this Success Criterion is to provide enough contrast between text and its background so that it can be read by people with moderately low vision.
$theme-colors: (
"primary": $primary,
"secondary": $secondary,
"success": $success,
"info": $info,
"warning": $warning,
"danger": $danger,
"light": $light,
"dark": $dark
) !default;
Let us take example of a simple button. Based on the
set, buttons are generated for each theme color variant in $theme-colors
using scss/_button.scss
mixin. This mixin receives a background color for which we need to generate corresponding contrast text color using button-variant
function. This function uses color-contrast
, which defaults to $min-contrast-ratio
4.5
to achieve WCAG 2 AA contrast ratio.@each $color, $value in $theme-colors {
.btn-#{$color} {
@include button-variant($value, $value);
}
}
In
color-contrast
function, the background color's contrast-ratio
is compared with foreground colors in following order:We can override the values of
$color-contrast-light
and $color-contrast-dark
which is set to default as $white
and $black
respectively. All these values are looped to find the suitable match. @function color-contrast($background, $color-contrast-dark: $color-contrast-dark, $color-contrast-light: $color-contrast-light, $min-contrast-ratio: $min-contrast-ratio) {
$foregrounds: $color-contrast-light, $color-contrast-dark, $white, $black;
$max-ratio: 0;
$max-ratio-color: null;
@each $color in $foregrounds {
$contrast-ratio: contrast-ratio($background, $color);
@if $contrast-ratio > $min-contrast-ratio {
@return $color;
} @else if $contrast-ratio > $max-ratio {
$max-ratio: $contrast-ratio;
$max-ratio-color: $color;
}
}
@warn "Found no color leading to #{$min-contrast-ratio}:1 contrast ratio against #{$background}...";
@return $max-ratio-color;
}
When the value of contrast ratio is greater than
$min-contrast-ratio
, that color is returned. Contrast ratio can be calculated by finding out relative luminance of both background and foreground color in current loop.@function contrast-ratio($background, $foreground: $color-contrast-light) {
$l1: luminance($background);
$l2: luminance(opaque($background, $foreground));
@return if($l1 > $l2, ($l1 + .05) / ($l2 + .05), ($l2 + .05) / ($l1 + .05));
}
WCAG algorithm to calculate relative luminance is used, which replaces yiq contrast algorithm in Bootstrap.
relativeLuminance (c) {
c = c / 255;
return c < 0.03928 ? c / 12.92 :
Math.pow((c + 0.055) / 1.055, 2.4);
}
SCSS implementation:
@function luminance($color) {
$rgb: (
"r": red($color),
"g": green($color),
"b": blue($color)
);
@each $name, $value in $rgb {
$value: if($value / 255 < .03928, $value / 255 / 12.92, nth($_luminance-list, $value + 1));
$rgb: map-merge($rgb, ($name: $value));
}
@return (map-get($rgb, "r") * .2126) + (map-get($rgb, "g") * .7152) + (map-get($rgb, "b") * .0722);
}
Here,
$_luminance-list
is a list of all possible value for Math.pow((c + 0.055) / 1.055, 2.4)
where c
lies between [0, 255], which is the range for any channel. This method is used to overcome the difficulty of not having a power function in SCSS as mentioned here. The same list is maintained in Bootstrap as well, with 4th decimal point. Refer here for more precise value. This The
red()
, green()
and blue()
are SCSS in-built functions to extract each channel's value for calculation.
opaque
is function, to remove the alpha channel by truncating it and mixing it with white in a ratio equal to alpha channel's value.@function opaque($background, $foreground) {
@return mix(rgba($foreground, 1), $background, opacity($foreground) * 100);
}
Few PRs are already resolved to achieve this contrast ratio like mentioned here and here.