How To Generate WCAG Compliant Contrast Color in Bootstrap

Written by itzsrikanth | Published 2021/01/15
Tech Story Tags: bootstrap | accessibility | sass | reverse-engineering | front-end-development | web-development | html5 | wcag

TLDR 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. Contrast ratio can be calculated by finding out relative luminance of both background and foreground colors in current loop. All these values are looped to find the suitable match. To achieve WCAG 2 AA contrast ratio, use color-contrast function to generate contrast text color. The color is returned to a color that matches the contrast ratio of foreground and background colors.via the TL;DR App

What is the importance of achieving the prescribed contrast ratio?

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
$theme-colors
set, buttons are generated for each theme color variant in
scss/_button.scss
using
button-variant
mixin. This mixin receives a background color for which we need to generate corresponding contrast text color using
color-contrast
function. This function uses
$min-contrast-ratio
, which defaults to
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:
  • $color-contrast-light
  • $color-contrast-dark
  • $white
  • $black
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.

References

  1. https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.htm
  2. https://github.com/twbs/bootstrap/pull/30168

Written by itzsrikanth | Passionate DivOps Engineer
Published by HackerNoon on 2021/01/15