Automating colour contrast ratios with Sass

Jonny Kates
5 min readApr 22, 2018

--

Recently, I was commissioned to help the UK’s leading charity and fundraising platform build a Sass theme framework.

Our challenge was to create a design architecture that would reduce an existing overhead of bespoke design for their CMS platform, and instead create a system of themes, that accept a series of client specific variables — for example, the brand’s primary colour — parse it through a selected theme, and spit out a unique CSS file that we’re able to use in production.

We decided that in order to keep things simple, the themes should accept as few client specific variables as we could get away with. Initially we created a couple of themes that could be customised via just two client brand colours, and a body font family.

However we were quickly presented with a major accessibility issue with this approach. Here’s how the scenario played out:

Q: What colour should links be?
A: Well I guess it should be the organisation’s $primary-colour
Q: And if their $primary-colour is a very pale, bright, yellow?..

Then we quite possibly hit an accessibility issue. What happens if that pale, bright, yellow fails a colour contrast ratio when on a white background? Then text links may not be visible for visually impaired users, and our theming architecture has failed.

This client’s $primary-colour — #FFD828 — isn’t accessible on white
The results from https://contrastchecker.com/

One simple solution might be to just add $link-colour as a new variable in our themes, and then manually assign an accessible colour for use of links that fits with the general aesthetic of the client’s brand.

However, we decided to take advantage of Sass’s pre-processing power and write a little test to check for colour contrast failures.

After some research, we found this post from 2013 by David Halford. In the post, David shares a Sass mixin that accepts a colour value and parses it through the W3C’s own formula for determining colour brightness. Halford was using this mixin to test whether text appearing on top a colour should be either black, or white; depending on the result of the colour’s brightness.

We did in fact end up using this exact mixin to check for button text colour. Our theme’s $button-colour was set simply to the client’s $primary-colour , so we needed to know whether the text on the button should be white or black, depending on how bright $primary-colour was.

However, we also needed a test that would take two specific colours: a $foreground-colour, and a $background-colour. And we needed to know whether $foreground-colour, would pass the accessibility colour contrast ratio from the W3C when placed upon $background-colour.

Alongside an algorithm for colour brightness that David Halford used was another algorithm from the W3C for checking the colour difference of two colours:

Color difference is determined by the following formula:
(maximum (Red value 1, Red value 2) — minimum (Red value 1, Red value 2)) + (maximum (Green value 1, Green value 2) — minimum (Green value 1, Green value 2)) + (maximum (Blue value 1, Blue value 2) — minimum (Blue value 1, Blue value 2))

The benchmark for a pass is a value of 500. So here is the Sass function that I wrote based on that algorithm:

Rather than returning either color: black or color: white , we now get a boolean result of the contrast test.

Next, we wanted to loop over this function, and if the test failed, then darken the $foreground colour by 1% until we reach the magic 500 benchmark for an acceptable colour contrast ratio.

I’ve made the output of above function slightly more verbose so you can see in the following dev-tools screenshot what is happening here:

We have to darken our original yellow by 30% until it contrasts with white enough to be accessible

So it looks like our Sass @while loop had to run a total of 30 times before we reached a tint of our original yellow that was dark enough to pass the contrast check on a white background. Here’s the contrastchecker.com result for our final value #8e7400 :

Not AAA admittedly, but I think I’m happy with this in terms of contrast. Here’s the results of the last failed value — i.e. just 1% lighter — #937800 :

A failure by comparison. Remember, the key number to compare is the ‘color diff’ values in the right most circle. A 500 is the benchmark here.

Because our Sass function allows for two colours, we can also adjust our darkness level by a different amount where we might be using a client’s $primary-colour on a different background colour other than white. For example, one of our themes had some key impact statistics on a light grey background; which it turns out requires our yellow colour to be darkened a further 5% :

And contrastchecker.com backs this up for us: the final fail value #7a6400 fails on our light grey, #f0f0f0 with a colour difference value of 498. But 1% darker passes:

Great! Now the final issue here — which I’m sure hasn’t escaped most of you — is that the result is often ugly as hell. Getting these mud brown links approved by the client could be a challenge (also show me a nice looking dark yellow; I’ll wait).

We could beat around the bush and change our function to say something like:

If you can’t get an accessible ratio within 15% darker, then we assume we’re getting into pretty dark colours here. So instead let’s run the client’s $secondary-colour through the test and use that as the $link-colour .

However, we decided that this was starting to rely too heavily on automation and sometimes we manually need a designer to come in and pick a lovely colour that is on brand and accessible. So we allowed for $link-colour (and $impact-statistic-colour) to be available as manual overrides if the results of our automated contrast check weren’t aesthetically pleasing.

Nonetheless, this colour contrast ratio has proven to be a really useful utility and helps make accessibility a first class citizen in our Sass framework.

--

--

Jonny Kates
Jonny Kates

Written by Jonny Kates

Frontend web guy living by the sea. Hire me! — http://jonny.lol

Responses (2)