Using Sass to automatically pick text colors

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.

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.

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

*/

@functionluminance($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

*/

@functioncontrast($back,$front){

$backLum:luminance($back)+ .05;

$foreLum:luminance($front)+ .05;

@returnmax($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.

*/

@functionchoose-contrast-color($color){

$lightContrast:contrast($color,white);

$darkContrast:contrast($color,black);

@if($lightContrast>$darkContrast){

@returnwhite;

}

@else {

@returnblack;

}

}

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!