Color contrast is math, not vibes
Somebody picked your site's colors in a meeting. They looked good on the projector, everyone agreed the gray text was tasteful, and the palette went into the style guide. Three years later a scan comes back and half the page is flagged.
The argument that follows is always the same: it looks fine to me. That argument can't be had. Contrast under WCAG is not an aesthetic judgment — it is a ratio produced by a fixed formula with fixed constants, and a color either clears the number or it doesn't.
The two numbers
1.4.3 Contrast (Minimum), Level AA, requires a contrast ratio of at least 4.5:1 between text and its background. Large text gets a lower bar: 3:1.
The definition of "large" is where people go wrong. WCAG defines large-scale text as at least 18 point, or 14 point bold — roughly 24px, or about 18.66px if the text is bold. Bumping a heading from 16px to 20px and leaving it regular weight does not qualify it; it still owes you 4.5:1. Bold at 20px does.
Two things are exempt: logotypes, and incidental text — decoration, invisible text, text in an inactive control, or text that's part of a picture with significant other visual content in it. Everything else on the page is in scope, including the gray placeholder text inside your search box.
(There is a 7:1 tier, 1.4.6 Contrast (Enhanced), but it's Level AAA. The Title II rule points at AA. Don't let a vendor sell you a failure against a standard you aren't held to.)
The formula
Contrast ratio is:
(L1 + 0.05) / (L2 + 0.05)
where L1 is the relative luminance of the lighter color and L2 the darker. Luminance itself is a weighted sum of the three channels, after each one is linearized out of sRGB:
L = 0.2126 * R + 0.7152 * G + 0.0722 * B
Those three coefficients are the whole story, and they are not equal. Green counts for 71 percent of perceived brightness. Blue counts for 7.
Here's what nobody tells the person choosing the palette
Take three colors that are numerically identical — one channel at CC, the other two at zero — and put each on white:
#0000CCblue: 11.22:1. Passes with room to spare.#CC0000red: 5.89:1. Passes.#00CC00green: 2.18:1. Fails, and not narrowly — it isn't close to the 3:1 large-text floor either.
Same numbers, same saturation, three completely different outcomes. This is why "our brand green" is the single most common contrast failure I find on city and county sites, and why the argument in the meeting never resolves: your eye is telling you those three colors are equally strong, and the formula is telling you green is nine times brighter than blue.
It cuts the other way too. The lightest neutral gray that passes on white is #767676, at 4.54:1. One step lighter, #777777, is 4.48:1 — a failure. A single hex digit separates a pass from a finding, which is exactly why this gets measured and not eyeballed.
The four places a checker won't answer for you
Automated contrast testing is genuinely good, and it's the part of an audit a machine can do well. It has four blind spots, and all four show up on municipal sites:
- Text over a photo or a gradient. There is no single background color to compute against. The scanner reports "cannot determine" and moves on. Your hero banner with white text over a picture of the courthouse has to be checked by hand, at the worst pixel, not the best one.
- Semi-transparent layers. If the text color is set to
#333333but sits inside a container withopacity: 0.7, the color that reaches the eye is not the color in the stylesheet. Some checkers read the declared value. - Text baked into an image. A scanned flyer or a banner graphic with words in it is, to the scanner, a picture. It is still text to a reader.
- Everything that isn't text. 1.4.11 Non-text Contrast, Level AA, requires 3:1 for the visual information needed to identify a control and its state — form field borders, focus outlines, toggle switches, icon-only buttons — and for the parts of a graphic you need to understand it. Pale gray input borders on a white form are a classic 1.4.11 failure that no text checker will flag.
One more, related: 1.4.1 Use of Color, Level A. If your in-body links are distinguished from the surrounding paragraph by color alone, color is carrying information by itself. The usual fix is an underline.
What to do
Pick the ratios before the palette, not after. Test the actual pairs you ship — text on card, text on shaded row, white on the brand color, link on body text — and record the numbers in the style guide next to the hex values so nobody relitigates it. Check field borders and focus rings against 3:1, and check the hero by hand. When the brand green fails, darken it for text and keep the original for fill where no words sit on it.
If you'd rather have the whole palette measured and the failing pairs replaced with ones that hold, that's a job I do.
Need this kind of thinking applied to your own setup? Get in touch →