Originally posted on Medium

Theming is always a challenge, particularly when you’re working on a library, rather than a standalone website. One example of an interesting issue that you’ll come across very frequently is choosing a text color that ensures readability and accessibility.

This is easy enough when you have known light or dark backgrounds; you can create two CSS classes and have users of your CSS library manually add them in depending on context.

Jekyll also offers powerful support for code snippets:

dark-background-contrast {
  color: white;
}
 
light-background-contrast {
  color: black;
}

However, things become a bit trickier if you want to add the concept of a theme color to your library, and make it easy to use. This color will very often be used as a background, and will need sufficient contrast for the text that’s placed on top.

theme-color-bg {
  background-color: #3f51b5;
}
 
theme-color-bg-contrast {
  color: white;
}

In the example above, theme-color-bg-contrast needs to change to a black text color if the user-defined theme color is light. While this is impossible to do in pure CSS (at least until the extremely useful CSS Color Module 4 is widely available), it can be done in Sass!

Working out the Sass

Here’s what the initial Sass should look like:

$theme-color: #3f51b5 !default;
 
theme-color-bg {
  background-color: $theme-color;
}
 
theme-color-bg-contrast {
  color: choose-contrast-color($theme-color);
}

Users can override the $theme-color, and depending on what they pick, the choose-contrast-color function will return either black or white.

So how do we implement choose-contrast-color? It turns out that all of the necessary calculations and minimum contrast rules are well defined in the WCAG 2.0 specification. There’s a whole section explaining visual contrast in detail, several links to ISO and ANSI standards, and even papers on the subject. Digging around for a while, I was able to find a published W3C technique with a clear description of the algorithm:

The algorithm for calculating color contrast
The algorithm for calculating color contrast

If you’re familiar with Sass, you’ll quickly notice an issue: the luminance calculations involve exponentiation, which isn’t available in the language or the standard library.

One solution to this would be to use the extensive Compass library, which not only includes exponentiation, but also the luminance operations we’re trying to implement. It requires Ruby, however, so it’s not an option for node-sass users. A pure Sass solution would be better.

Using old tricks

I was reviewing my math and exploring the possibility of using Newtonian approximation for the fractional parts of the exponent, until I had a chat with @wibblymat, who happened to be implementing an emulator at the time. He suggested a much simpler, old-school approach: using a lookup table!

The only part that involves exponentiation is the per-channel color space conversions done as part of the luminance calculation. In addition, there are only 256 possible values for each channel. This means that we can easily create a lookup table.

An excerpt of the lookup table
An excerpt of the lookup table

You can take a look at the full table that I generated for the MDC-Web project to avoid having to generate your own.

With the channel values calculated, we can implement the rest of the algorithm easily:

/**
 * Calculate the luminance for a color.
 * See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
 */
@function luminance($color) {
  $red: nth($linear-channel-values, red($color) + 1);
  $green: nth($linear-channel-values, green($color) + 1);
  $blue: nth($linear-channel-values, blue($color) + 1);
 
  @return .2126 * $red + .7152 * $green + .0722 * $blue;
}
 
/**
 * Calculate the contrast ratio between two colors.
 * See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
 */
@function contrast($back, $front) {
  $backLum: luminance($back) + .05;
  $foreLum: luminance($front) + .05;
 
  @return max($backLum, $foreLum) / min($backLum, $foreLum);
}
 
/**
 * Determine whether to use dark or light text on top of
 * given color.
 * Returns black for dark text and white for light text.
 */
@function choose-contrast-color($color) {
  $lightContrast: contrast($color, white);
  $darkContrast: contrast($color, black);
 
  @if ($lightContrast > $darkContrast) {
    @return white;
  }
  @else {
    @return black;
  }
}

That’s it! We now have contrast calculation in Sass, and we’re automatically picking black or white text, depending on which provides the most contrast. This can have a huge impact on readability, particularly for users with low vision.

This solution only requires an extra, pre-calculated constants file that will never change. It works with any Sass implementation, and it won’t bloat your CSS since it’s only used at build time. Pretty neat!

Next time, I want to look at how to do the same thing in JavaScript, at runtime, so you can have dynamic theming with CSS custom properties. See you then!

Edit: here’s a Sassmeister live demo of the technique above.