WCAG Contrast — Passes Tool, Fails in Sunlight

webdev a11y wcag color-contrast tools

The design system badge says “AA.” The contrast checker is green. A teammate still cannot read the secondary label on a park bench at noon. WCAG ratios are necessary; they are not a sunlight simulator. Treat 4.5:1 as a floor for body text, not a trophy.

What the numbers mean

Contrast ratio compares relative luminance of foreground and background, from 1:1 (same) to 21:1 (black on white).

LevelNormal textLarge text
AA≥ 4.5:1≥ 3:1
AAA≥ 7:1≥ 4.5:1

“Large” is about rendered size and weight (roughly 18pt+ or 14pt+ bold), not the CSS keyword large. If marketing sets font-size: 15px and calls it a hero, it is still normal text for WCAG. Measure what users get after responsive type scales down.

Non-text UI (icons, input borders, focus rings that convey state) generally needs 3:1 against adjacent colors under WCAG 2.1. Decorative flourishes are exempt; meaning-bearing chrome is not.

Why lab passes fail outdoors

  • Brightness caps — users turn phones down; your soft gray collapses
  • Reflections — polarizing glare eats mid-contrast pairs first
  • Anti-aliasing — light text on mid backgrounds gets optically thinner
  • Overlaysrgba scrims and images behind text change the effective background the Figma frame never showed
  • Color blindness — contrast ≠ hue distinction; do not rely on red/green alone

Always measure the actual pixels behind the glyphs (including images and gradients), not the token sitting in an empty auto-layout frame. Use a color contrast checker on pairs sampled from DevTools after themes apply.

Gray-on-gray and “brand soft”

text #737373 on bg #F5F5F5  → often ~3.x:1  (fails AA normal)
text #525252 on bg #F5F5F5  → closer to 4.5–5:1

Designers love washed chrome. Body copy should not live there. Keep muted colors for borders and placeholders; keep primary reading text darker (or backgrounds lighter). Placeholder text that matches disabled labels trains users to ignore both.

ElementFrequent mistakeBetter approach
PlaceholderSame gray as disabled labelDarker placeholder or visible label above the field
DisabledUltra-low contrast “to look disabled”Opacity and keep text ≥ 3:1 if it must remain readable
LinksColor aloneColor + underline or weight so contrast is not the only cue
Focus1px light-blue ring on white3:1+ ring or dual-color focus indicator

Dark mode is a second audit

Inverting a light theme rarely preserves ratios. Soft gray text on #121212 may pass AA yet feel weak on OLED glare. Check hover, active, and visited states independently — each pair can fail while the resting state passes.

AA vs AAA without audit theater

Ship AA as the floor for product UI. Push AAA where people read for a long time (docs, legal, education). Do not claim AAA site-wide if charts, maps, and disabled states cannot comply — auditors notice theater.

Pair automation (axe, Lighthouse, CI contrast checks on tokens) with two manual checks: dark mode tokens, and a phone screenshot in daylight. Automation will not catch “passes 4.51 but feels illegible.”

A concrete remediation loop

  1. Pick failing selector from DevTools.
  2. Sample foreground and true background (eyedrop after overlays).
  3. Raise text luminance contrast until ≥ 4.5:1 (or ≥ 3:1 if truly large).
  4. Re-check hover/focus/dark variants — they fail independently.
  5. Update the design token, not one-off hexes in a single component.
  6. Add a regression screenshot or token unit test so the soft gray does not return next quarter.

Contrast is a ratio, a viewing condition, and a content decision. Clear the AA bar in tools, then give body text a little headroom so sunlight — and a dim commute — do not undo the audit.