Contrast & Accessibility

WCAG 2.x contrast ratios, APCA, and how to pick readable color pairs.

Contrast is where color stops being aesthetic and becomes functional: it decides whether people can read your interface at all. Two systems matter today - the WCAG 2.x contrast ratio that regulations reference, and APCA, the perceptually accurate model built for WCAG's successor.

Why contrast is the first check#

Reading depends almost entirely on luminance contrast- the light/dark difference between text and background. Hue contrast barely helps: red on green with equal luminance is nearly unreadable despite being “opposite” colors. Low vision, aging eyes (contrast sensitivity roughly halves by age 65), bright sunlight, and cheap panels all shrink the readable range further.

The quick brown fox
gray on white · 4.54:1 · passes AA
The quick brown fox
light gray on white · 2.32:1 · fails AA
The quick brown fox
red on green (equal-ish luminance) · 1.6:1 · fails AA
Live samples. Luminance difference - not hue difference - is what makes text readable.

WCAG 2.x contrast ratios#

WCAG 2.x compares the relative luminance of the two colors: (L1 + 0.05) / (L2 + 0.05), giving a ratio from 1:1 (identical) to 21:1 (black on white). The thresholds:

LevelNormal textLarge text (≥24px / ≥18.7px bold)UI components
AA (required)4.5:13:13:1
AAA (enhanced)7:14.5:1-

AA is the level laws and procurement standards typically cite (ADA settlements, the EU Web Accessibility Directive, Section 508). Treat 4.5:1 as a floor for body text, not a target.

Where the ratio falls short#

  • It ignores polarity - light-on-dark and dark-on-light score identically, but dark mode needs more contrast to read equally well (glare and halation around light text).
  • It misjudges the mid-range - some passing 4.5:1 pairs are genuinely hard to read, while some failing pairs (especially dark-mode oranges) read fine. The formula predates modern research on supra-threshold contrast.
  • It knows nothing about fonts - a 4.5:1 hairline thin font is far less readable than 4.5:1 at a sturdy weight.

APCA: contrast, modernized#

The Accessible Perceptual Contrast Algorithm (APCA), developed for the draft WCAG 3, models perceived lightness contrast directly. It outputs Lc (lightness contrast) from ~0 to ~106, is polarity-aware (dark mode scores differently), and is meant to be read against font size and weight:

Lc scoreRough use
Lc 90+Body text at small sizes; the AAA-like tier
Lc 75Minimum for body text
Lc 60Minimum for large/bold text (≈ AA 4.5:1 in spirit)
Lc 45Headlines, large UI text (≈ AA 3:1 in spirit)
Lc 30Non-text UI elements, placeholders
Lc 15Absolute floor for any visible element

In practice#

  • Decide text color per shade, systematically. In OKLCH, backgrounds with L ≳ 0.65–0.7 usually want dark text and below that white text - one threshold, applied everywhere.
  • Never encode meaning in color alone - pair state colors with icons or labels (this overlaps with the Color Blindness chapter).
  • Check disabled and placeholder states - they are the most common real-world failures, sitting in the 2–3:1 zone.
  • Test dark mode separately - inverting a palette does not preserve its contrast, and APCA will score it differently.
tokens.css - a contrast-safe pairing baked into tokens
:root {
  --surface: oklch(0.97 0.01 250);
  --on-surface: oklch(0.25 0.02 250);   /* 12.6:1 on --surface */

  --primary: oklch(0.55 0.19 250);
  --on-primary: oklch(0.99 0 0);        /* 5.4:1 on --primary  */
}

Further reading#