What is WCAG?
Let's start with a summary of why we have a formula. If you're comfortable with the accessibility guidelines, jump straight to the next part.
The web content accessibility guidelines (WCAG) tell you how you can make your website accessible to disabled users. Certain websites must follow them in some places, like the public sector in Europe.
Each guideline includes some "success criteria", which are narrow, measurable requirements. A website must follow every criterion to conform to the guidelines at a certain level.
Three of these criteria concern contrast; 1.4.3 Contrast (Minimum), 1.4.6 Contrast (Enhanced), and 1.4.11 Non-text Contrast. To measure the conformance to these guidelines, we need a non-subjective way to measure contrast.
Before we look at exactly how we measure it, let's look at what a color on the web actually contains.
How do we describe colors?
The web has settled on sRGB as a standardized color space. This means colors are encoded in such a way that they convert into their red, green and blue components, like the screens showing them use.
Each colour can have 0 to 255 parts of each component. These are 8-bit values, because bits are binary (0 or 1) units, and 8 bits has possible values. So, there are a total of colors.
There are several ways to encode the same color information which work in HTML and CSS. We'll consider two.
If you write your colors in RGB, you'll see this in the numbers: rgb(25, 100, 210)
means a color that contains 25/255 red, 100/255 green and 210/255 blue. So blue with a little green and the tiniest bit of red. RGB is the most intuitive to me.
If you prefer hex codes, note that they convert. If you know hexadecimal (base 16). #1964D2
is the same color, because 0x19 (19 in hexadecimal, so ) is the same as 25 in decimal. 0x64 (). and 0xD2 (D is the digit for 13, so ). #1964D2
is a more compact representation of rgb(25, 100, 210)
.
Black has no parts of red, green or blue, while white has all available equal parts of all three. Any color that has equal red, green and blue will appear grey, like rgb(100, 100, 100)
or #CCCCCC
.
Finding the relative luminance
If we want to calculate the contrast, we'll need to start with the relative luminance.
The relative luminance formula is included in the glossary of WCAG, but let's repeat it here so we can see it step-by-step.
First, every 8-bit color component value (the ones that can be 0-255) is converted into a number between 0 and 1 by dividing it by 255.
Next, depending on the values we get, we transform the component. We do the same for every color independently. At the end, we have values R, G and B.
If then else
If then else
If then else
Now, we bring all the values from one color together to find the luminance.
Each luminance can be between 0 and 1, inclusive.
The value is a good representation of how the color might look in grey.
Getting a contrast ratio
When we divide the higher luminance (let's call it ) by the lower ().
We add 0.05 to both values. This gives us a nice pair of limits to the output values. If the luminances are equal, we get a 0.05/0.05, so 1. If the two luminance values are 0 and 1, the limits, we end up with 1.05/0.05, which is 21. There's no way to get a bigger value.
Since we often talk in ratios, you'll want to write it ":1". Luckily, convention says we don't mind combining a decimal and a ratio here.
This is tedious math. Computers are good at tedious math, so you'll find tools to do this instantly built into every modern browser and all over the web. You'll never have to calculate one manually.
Why does it work?
It makes sense that increasing the intensity of each component makes the image lighter.
Looking back at the luminance formula; the same size change of the G channel is worth many more times the same size change in the blue. To appreciate why, let's see them side-by-side.
Let's start with a dark grey (30/255 on all channels), and try adding either 100 to the green or the blue:
rgb(30, 30, 30)
rgb(30, 130, 30)
rgb(30, 30, 130)
The green ends up a bit lighter, but it's a subtle difference compared to the blue.
For an even bigger difference, let's try and maximum value of each channel in isolation:
rgb(255, 0, 0)
rgb(0, 255, 0)
rgb(0, 0, 255)
The green looks really bright, while the blue looks darker.
So it's clear that the different channels have different effects on luminance.
The contrast calculation doesn't consider what the color is, only the luminance. Aren't some colors higher contrast than others?
According to WCAG, this is intentional. Research from 1991 found that luminance affects reading performance much more than hue or saturation. The full Knoblauch et al. paper is available via ResearchGate.
What should the number be?
To conform with WCAG, 3:1 is the smallest you should use anywhere. It's allowed for non-text items and large text (over 18pt or 14pt bold). All other text needs to be 4.5:1 or more (there are a couple of exceptions too, check the precise requirements in Understanding contrast (minimum)).
At AAA level, WCAG requires 4.5:1 for large text and 7:1 for all other text.
WCAG only mentions helping users with low vision as the aim here, so there is no upper limit. There are many users who state that too much contrast is bad for their experience. Migraines and Autism are often mentioned, but I've been unable to find scientific research quantifying the impact. I hope some more research will appear in the future to suggest an upper limit.
When I build things for myself, I keep 4.5:1 as the lower limit, around 17:1 as an upper limit, and try to keep most things around 10:1. This is a pretty arbitrary choice, and sometimes quite limiting.
On this website, I include a low contrast option. There, most contrast ratios are around 4.5:1, so still allowed at WCAG 2.1 AA.
The precise value you settle on is your choice, but by checking the formula, you can always figure out how to tweak a color to make it a tiny bit more readable when you need to.