I couldn't read it in the sun
A contrast ratio is a floor that everybody reads as a target, and in the end it took a photograph to move the gray.
I took my phone out to the front steps last week to look at a page I’d built. I couldn’t read it. Light gray body text, thin weight, on white. I turned the brightness up, then moved into the shade, then gave up and went back inside.
I assumed I’d broken something, so I checked it properly. Every pair on the page cleared 4.5:1. A couple of them cleared 7. The checker had nothing to say about any of it.
Then I asked the designer to take the body gray a couple of steps darker. They pointed out, fairly, that it passes. Which, of course, it does. And I’m the one who put that number in the project’s acceptance criteria back in the spring, on the theory that a number beats an argument about taste.
Turns out a number written down as a floor shows up at the other end as a target.
Some light math
Briefly, because the mechanism explains the failures.
The WCAG guidelines define contrast as a ratio between the relative luminance of two colors. Luminance comes out of the red, green and blue channels with fixed weights (roughly 21% red, 72% green and 7% blue). The two values give you a ratio somewhere between 1:1 and 21:1.
Body text needs 4.5:1. Large text needs 3:1, and since 2.1 so do interface pieces like input borders and icons.
That’s the whole thing. Two colors in, one number out.
Now go and look at what isn’t in that sentence. The size of the type, its weight, the face it’s set in, the screen it lands on, the room the screen is in, and whether the background behind it is even a flat color.
Four blind spots
Weight is the one that got me. A ratio of 4.5:1 in a 300 face at 14px and the same 4.5:1 in a 600 face at 18px are not the same reading experience. The guidelines have one coarse allowance for large text and nothing at all for weight underneath it. Thin type at small sizes passes routinely and is punishing to read.
The page on the steps was set in a light weight. That was probably more of the problem than the color was.
Hue is the strangest one. The channel weights approximate how the eye responds, and they were tuned for the displays of the early nineties. They aren’t kind to blue.
Pure blue on white lands around 8.6:1, which sails past even the strict level. A paragraph of it is horrible to read. I don’t think anybody who has actually looked at that paragraph believes it’s nearly twice as readable as a gray at 4.5:1. But the number says it is.
Then there’s the screen and the room, which is the pair I’d been quietly ignoring for years. The ratio assumes a display behaving as specified, being looked at in conditions nobody specified.
A phone at 30% brightness in August sun, an aging laptop panel with a warm-shifted white point, a projector in a meeting room with the lights up. All of those compress the range and none of them are in the math. And the design was approved on two very good monitors in a room with the blinds down.
And then there’s anything that isn’t a flat color behind the text. Text over a photograph has a different ratio at every pixel. The standard advice is to check the worst part. Nobody does, and whatever you measured stops being true the moment somebody swaps the image out in the CMS.
If text sits on a photograph it wants a scrim or a panel under it. That’s a design decision, not a measurement.
In fairness to the number
The formula is also too strict sometimes, in a direction that pushes people toward worse work. Some saturated pairs fail while being perfectly legible (oranges and yellow-greens especially). The usual response is to darken the brand color until it goes muddy. That’s a real cost paid to a number, and I’ve watched it get paid.
But when my eye and the number disagree and the number says fail, the number wins. Every time. Mine is one pair of eyes in one room, belonging to somebody whose vision is currently fine. “Looks alright to me” is exactly the reasoning the standard exists to overrule.
As far as I can tell, every blind spot above runs in the direction of the number being too generous. I haven’t found a case yet where ignoring a failure turned out to be the right call.
Headroom
So it’s a floor. 4.5:1 is the least you can do without anybody taking you to task over it. It isn’t a goal.
For anything a person has to read for more than a few seconds I go for 7:1. It costs almost nothing on most palettes and it buys back all four of those blind spots at once.
I’ve also stopped putting thin weights anywhere near the bottom of the range. If the type is 300 I want a lot of headroom or a heavier face. For body copy I’ve more or less stopped going under 400.
It helps to memorize where the edge sits. Designers reach for a gray around #999 constantly. It looks fine on a good monitor and it’s not even close.
/* 2.85:1 on white, fails */
color: #999;
/* 4.54:1 on white, the lightest gray that passes for body text */
color: #767676;
Then check the muted text in both themes, since a gray that sits comfortably as a secondary on white is usually too dim on near-black. I wrote that down last year and then went and got it wrong again anyway, which is about the going rate for things I write down.
Go outside
None of which found the actual problem. I found it on the front steps with a phone, not looking for anything.
So that’s a step now. Once a page is more or less put together, I take it outside (or to a window if I can’t get out) and check it at whatever brightness the phone happens to be on. It takes 30 seconds and needs no tooling, and it’s the only check I run with the room in it.
It’s the same move as going through a page on the keyboard, which I started doing to my own work last month. That one turned up something no tool had ever mentioned to me. Both of them find more than the automated pass does, which I’d have called an unfair comparison in the spring and now think is just true.
Going outside is what finally settled the gray. I couldn’t argue the designer out of a number I had personally written into the acceptance criteria, so I stopped trying. I took a photo of the phone in the sun with the page on it and posted that in the project channel. The gray was two steps darker by that afternoon.
Nobody mentioned the ratio at all, in either direction. I think that was the most honest part of the whole exchange.
They’re working on a better one
Worth knowing about, if only so nobody treats the current numbers as permanent. There’s active work on a successor that accounts for size and weight and uses a perceptual model instead of the old luminance math. It also treats light-on-dark differently from dark-on-light, which the current one doesn’t do at all. It gives some pretty different answers in places, especially for light text on dark backgrounds and for exactly the thin-type case that started all this.
It’s a draft and it’ll be years before anybody has to comply with it, so I’m not designing against it yet. But it’s the clearest confirmation I’ve found that the gap between passing and readable is a known gap, and not just me being precious about my phone.
Keep testing contrast in the meantime. I’d be far more worried about somebody deciding the whole exercise is theater than about somebody trusting the number too much.
It catches a lot and it takes seconds. It’s also one of the very few accessibility checks a machine can actually do, which is more than you can say for the alt attributes it also reports zero errors on. It belongs in every pass, next to the other quick ones.
I’ve stopped treating a green column as the end of the conversation. It’s a lower bound on one variable, computed for a screen I’m not looking at in a room nobody is standing in. And the page it approved was one I couldn’t read on my own front steps. I still run it on everything. I’ve just stopped handing anybody a screenshot of it like it’s the answer.