← All writing
Craft · · 7 min

Contrast ratios lie

On a formula from the CRT era, the four things it cannot see, the one case where it's wrong in the other direction, and what to do while a better one is being worked on.

Accessibility CSS

In June I was sitting outside, in bright sun, trying to read a page I’d built. Light gray text, a nice thin weight, on white. Absolutely fine on my monitor. Genuinely, actually unreadable outdoors, to the point where I gave up and went in.

I checked it afterward, because I assumed I’d missed something. It passed. Every pair on the page cleared the threshold. The tool was green.

I’ve been carrying the number around for years as though it were the answer, and it isn’t the answer, it’s a floor with some fairly serious blind spots. Which is worth saying carefully, because there’s a version of this post that talks somebody out of testing contrast at all, and that isn’t the version I want to write.

What the number measures

Briefly, because the mechanism explains the failures.

The guidelines define contrast as a ratio between the relative luminance of two colors. Luminance is computed from the red, green and blue channels with fixed weights, roughly 21% red, 72% green, 7% blue, and then you take the two values and produce a ratio between 1:1 and 21:1. Body text needs 4.5:1, large text needs 3:1, and since the 2.1 update, so do interface components like input borders and icons.

That’s it. Two colors in, one number out.

Look at what isn’t in that sentence: the size of the text, its weight, the typeface, the background it’s sitting on if it isn’t flat, the screen, or the room.

The four things it can’t see

Weight. A ratio of 4.5:1 in a 300-weight face at 14 pixels and the same ratio in a 600-weight face at 18 pixels are not remotely the same reading experience. There’s a coarse allowance for “large text” and nothing at all for weight below that threshold, so thin type at small sizes passes routinely and is punishing. My beer garden page was a light weight, and that was more of the problem than the color.

Hue. The formula’s channel weights are a rough approximation of how the eye responds, made for the displays of the early nineties, and they’re notably unkind to blue. Pure blue on white has a ratio of about 8.6:1, which sails past the strictest level, and it’s genuinely horrible to read a paragraph of. Nobody who has looked at it thinks it’s four times easier than a gray at 4.5:1, and the number says it is.

The screen and the room. The ratio assumes a display behaving as specified in a room you’re not in. A phone at 30% brightness in sunlight, a five-year-old laptop panel with a warm-shifted white point, a meeting-room projector: all of these compress the range and none of them are in the calculation. Meanwhile the design was approved on two very good monitors in a room with the blinds down.

Anything that isn’t a flat color behind it. Text over a photograph has a different ratio at every pixel, and the answer people give is “check the worst part,” which nobody does, and which fails anyway when the client swaps the image in the CMS. If text sits on an image, it needs a scrim or a panel, and that’s a design decision rather than a measurement.

Pure blue on white clears the strictest threshold with room to spare, and nobody who has read a paragraph of it believes the number.

And once, in the other direction

The honest counterweight, because I don’t want this to read as license.

The formula is also occasionally too strict, in ways that push designers toward worse decisions. Certain saturated pairs, oranges and yellow-greens especially, fail while being perfectly legible, and the standard response is to darken the brand color until it goes muddy.

More importantly: when a designer’s eye and the number disagree, and the number says fail, the number wins. Every time. My eyes are one pair of eyes, in one room, belonging to a person whose vision is currently fine, and “it looks alright to me” is precisely the reasoning the standard exists to overrule. The blind spots I’ve listed all run in the direction of the number being too generous. I’ve yet to find one where ignoring a failure was justified.

What I actually do now

Treat it as a floor rather than a target. 4.5:1 is the minimum legal-ish requirement, not a design goal. For anything anyone has to read for more than a few seconds I aim for 7:1 or better, which costs nothing on most palettes and buys back all four of the blind spots at once.

Stop pairing thin weights with anything near the floor. If the type is 300 weight, I want a lot of headroom, or I want a heavier weight. On body copy I’ve more or less stopped using anything under 400.

Know where the edge actually is. On white, the lightest gray that passes for body text is around #767676, which is a useful number to have memorized because designers reach for grays around #999 constantly and it doesn’t pass and it looks fine on their screen.

Check the muted text in both themes, since a gray that’s a comfortable secondary on white is often far too dim on near-black, which was the thing that caught me out last year.

And then the unscientific bit that has caught more real problems than any of the above: look at it on a cheap phone, at half brightness, outdoors, or at least by a window. 30 seconds, no tooling, and it’s the only test that includes the room.

The formula is being replaced

Worth knowing, if only so you don’t take the current numbers as permanent.

There’s active work on a successor that accounts for font size and weight, uses a perceptual model rather than the old luminance math, and treats light-on-dark differently from dark-on-light, which the current one doesn’t at all. It produces quite different answers in places, particularly for light text on dark backgrounds and for the thin-type case that started this post.

It’s a working draft and it’ll be years before it’s a requirement, and I’m not designing against it yet. But it’s the clearest confirmation I’ve seen that the gap between “passes” and “readable” is real and known, rather than me being fussy about a beer garden.

Keep testing it anyway

Keep testing contrast. It catches a great deal, it’s automatable, and it’s one of the few accessibility checks that can be. I’d be much more worried about somebody reading this and skipping it than about somebody trusting it too much.

Just don’t let the green tick be the end of the conversation. It’s a lower bound on one variable, computed for a screen you’re not looking at, in a room nobody’s standing in.

Read similar posts
7 min

Dark mode is a data problem

A client asked how long a dark mode would take and I said half a day, because I was thinking about colors, and the colors were about an hour of it.

6 min

Motion is not decoration

I built a parallax hero because a client asked for one, showed it to a colleague, and they asked me to close the tab, and then had a headache for the rest of the afternoon.