The most common accessibility failure I see in audits isn’t missing alt text or a keyboard trap. It’s light gray text. It looks elegant in a design file on a good monitor, and it’s unreadable on a phone outdoors or for anyone with low vision. WCAG turns “readable enough” into a number, and once you understand how that number works, the fixes become mechanical.
The examples below come from the ByteKiln contrast checker.
The formula
WCAG 2 defines contrast as a ratio of relative luminances:
ratio = (L_lighter + 0.05) / (L_darker + 0.05)
L = 0.2126 R + 0.7152 G + 0.0722 B (linearised sRGB channels)
It ranges from 1:1 (the same color twice) to 21:1 (black on white). The weights say something important: green contributes most to perceived brightness and blue least, which is why blue text on black is so hard to read.
The thresholds
| Requirement | Ratio | Success criterion |
|---|---|---|
| AA, normal text | 4.5:1 | 1.4.3 |
| AA, large text | 3:1 | 1.4.3 |
| AAA, normal text | 7:1 | 1.4.6 |
| AAA, large text | 4.5:1 | 1.4.6 |
| UI components and graphics | 3:1 | 1.4.11 |
AA is what most laws and policies reference. The 3:1 for UI components covers things people forget: input borders, icon-only buttons, focus indicators, chart lines.
What counts as large text
WCAG defines large text as at least 18 point, or 14 point bold. In CSS pixels that’s 24px regular or about 18.66px bold. So a 20px heading at normal weight is not large text and needs 4.5:1, while an 18.66px bold label is. The checker asks for your font size and weight and applies the right threshold.
Why 4.48 fails
Here’s the example I use to explain it:
#767676 on #FFFFFF → 4.54:1 (AA pass)
#777777 on #FFFFFF → 4.48:1 (AA fail)
#767676 is famous as the lightest gray that passes AA on white; one step lighter fails. The comparison uses the exact ratio: #777777 is 4.478:1, and WCAG doesn’t allow rounding up. Some tools display two decimals and round to 4.50, which is where “it says 4.5, why does it fail?” comes from. The checker always decides pass or fail on the unrounded value, and when the displayed number would round to a threshold that it doesn’t actually meet, it says so explicitly.
Fixing a failing pair without redesigning
Tailwind v3’s blue-500, #3B82F6, on white is 3.68:1. Fine for large text and UI borders, not for body text. Picking a darker blue by eye tends to overshoot. The checker instead searches OKLCH lightness while keeping the hue and chroma, and returns the closest color that passes:
#3B82F6 on #FFFFFF → 3.68:1
suggested #2C72E5 → 4.52:1 (AA normal)
It’s recognisably the same blue, 4.9% darker in perceived lightness. The search checks the final hex value too, because rounding to 8-bit channels can drop a ratio from 4.502 to 4.498.
Sometimes there’s no answer: against a mid-gray background, no lightness of a given hue reaches 7:1. The checker tells you that rather than suggesting a color that still fails.
Transparency changes everything
WCAG’s formula has no alpha channel, so translucent colors need to be blended first:
- Translucent text —
rgb(0 0 0 / 0.55)on white — is composited over the background, then measured. The preview shows the blended color. - Translucent backgrounds depend on what’s behind them. A 10% white overlay is nearly invisible on white and very visible on black. The checker asks for the color underneath (defaulting to white) instead of guessing.
This catches a common bug: text on a semi-transparent card that passes in light mode and fails in dark mode, because the card itself changed contrast.
What about APCA?
APCA, the Accessible Perceptual Contrast Algorithm, is the method being developed for WCAG 3. It returns a lightness contrast value (Lc) rather than a ratio, treats dark-on-light and light-on-dark differently, and pairs with font size and weight tables. It’s genuinely better at predicting readability — WCAG 2 overrates light text on dark backgrounds, for example.
But WCAG 3 isn’t a standard yet, and no law references APCA. The checker shows the APCA Lc value as an optional, clearly labelled draft readout. For compliance, use the WCAG 2 ratio.
Where contrast checks fit
I check contrast in three places:
- When choosing tokens — every text/background pair in the design system, in both themes.
- When a component uses transparency or images behind text.
- In automated audits. The Accessibility Checker runs axe-core over HTML and flags contrast failures along with everything else.
If your colors are in oklch() or hsl() rather than hex, paste them straight in — the checker accepts every format the color converter does, and the converter’s “Check contrast” link opens the contrast checker with the color already filled in.