Choosing accessible text colours

Contrast is the accessibility requirement most likely to be checked and most likely to be got wrong. The mechanics are simple; the judgement calls are not.

How WCAG 2 computes a ratio

The formula has two steps. First, each colour’s relative luminance is computed from its linear RGB values:

L = 0.2126 × R + 0.7152 × G + 0.0722 × B

Those coefficients reflect the eye’s sensitivity: green contributes most of the perceived brightness, blue barely any. Note that the channels must be linearised first — using encoded values here is a common implementation bug that produces plausible-looking but wrong numbers.

Then the ratio between two colours:

ratio = (lighter + 0.05) / (darker + 0.05)

The 0.05 models ambient light reflecting off the screen. It also bounds the result: pure black on pure white gives exactly 21:1, and identical colours give 1:1.

The thresholds

Level Body text Large text Non-text
AA 4.5:1 3:1 3:1
AAA 7:1 4.5:1

“Large” means 24px regular or 18.66px bold and above. Non-text covers things like input borders, focus rings and meaningful icons.

Where WCAG 2 goes wrong

The formula is a decent approximation with two well-documented weaknesses.

It overestimates contrast on dark backgrounds. Light text on a mid-dark background often passes comfortably while being noticeably harder to read than the number suggests. Dark-mode designs built purely to the number frequently feel washed out.

It ignores font weight and size beyond the crude large-text cut-off. A 300-weight 14px label and a 700-weight 14px label have very different legibility at the same ratio, and the formula cannot tell them apart.

What APCA does instead

APCA — the Accessible Perceptual Contrast Algorithm, developed for the WCAG 3 draft — models perception more carefully. Two differences matter in practice.

It is polarity-aware. Dark text on light backgrounds and light text on dark backgrounds are computed differently, because they genuinely do not behave the same way. The sign of the result tells you which case you are in.

Its output is Lc, roughly −108 to 106, tied to usage rather than a pass/fail line:

Lc Suitable for
90+ Body text at any size
75+ Body text from 18px
60+ Headings and large text
45+ Large headings only
30+ Non-text elements
under 15 Effectively invisible

Every colour page on this site shows both metrics side by side, so you can see where they agree and where they diverge.

A practical method

Start from the background. Text colour is constrained by what it sits on, so fix the surface first.

Work in OKLCh and adjust lightness. Because OKLCh lightness is perceptual, moving it is the most direct way to change contrast without changing the colour’s character. Dropping L by 0.1 has a predictable effect; fiddling with hex digits does not.

Do not chase 21:1. Pure black on pure white is maximum contrast and many people find it harsh — it can cause halation for readers with astigmatism. Something like #1a1a1a on #fafafa lands around 17:1 and reads more comfortably.

Check both metrics. If WCAG 2 says 4.6:1 and APCA says Lc 55, you have technically passed AA with text that will be hard to read as body copy. Trust the disagreement as a signal to look again.

Test the real thing. Contrast on a colour swatch is not contrast on your actual font at your actual size against your actual background image.

Generating a compliant scale

If you need a palette where certain steps are guaranteed to work as text, generate the scale in OKLCh and check as you go. A useful convention:

  • Steps 50–200 as backgrounds for dark text
  • Steps 500–600 as accents and borders
  • Steps 700–950 as text on light backgrounds

Check contrast on each step as you build the scale — the picker can help you read ratios for individual colours as you go.

Questions

What contrast ratio do I actually need?

WCAG 2 level AA needs 4.5:1 for body text and 3:1 for large text, which means 18.66px bold or 24px regular and above. Level AAA raises those to 7:1 and 4.5:1. Non-text elements such as input borders and icons need 3:1 under WCAG 2.1.

Should I use WCAG 2 or APCA?

Use WCAG 2 for compliance, because that is what audits and legislation reference today. Use APCA as a sanity check, because it handles mid-tone backgrounds and thin fonts far better. When the two disagree sharply, look at the design and trust your eyes.

Does contrast apply to disabled controls?

WCAG 2 exempts inactive controls from the contrast minimum. That said, users still need to read what a disabled button says, so treating the exemption as permission to use very low contrast tends to produce complaints.

More guides