Color Contrast Requirements
WCAG contrast ratios for text and UI components, large text thresholds, tools to measure, and design-token strategies that stay compliant.
- accessibility
- color-contrast
If text doesn’t contrast enough with its background, low-vision users can’t read it. WCAG contrast minimum (1.4.3) and non-text contrast (1.4.11) set numeric targets. Designers pick pretty grays; engineers must enforce ratios in tokens and components.
Docs: Understanding 1.4.3, 1.4.11 Non-text contrast.
Text contrast (AA)
| Text | Minimum ratio (AA) |
|---|---|
| Normal text | 4.5:1 |
| Large text | 3:1 |
Large text ≈ 18pt regular or 14pt bold (roughly 24px / 18.66px — verify with your CSS px/pt mapping).
AAA is stricter (7:1 normal, 4.5:1 large) — excellent for body copy when brand allows.
Non-text contrast (AA)
UI components and graphical objects needed to understand the UI need 3:1 against adjacent colors — icons, input borders, focus indicators (with caveats in the understanding doc).
/* Weak */
.input { border: 1px solid #ddd; } /* often fails on white */
/* Stronger */
.input { border: 1px solid #767676; }
Measure, don’t guess
Tools:
- Browser DevTools contrast badges
- WebAIM Contrast Checker
- Figma plugins
- axe color-contrast rule (computed styles)
<p style="color: #777; background: #fff">May fail normal text AA</p>
Design tokens
:root {
--text: #1a1a1a;
--text-muted: #595959; /* verify 4.5:1 on --surface */
--surface: #ffffff;
--border: #737373;
--focus: #005fcc;
}
Document approved pairs (text on surface, text on brand, inverse). Don’t invent one-off hex in components.
States
Check hover/active/disabled carefully:
- Disabled text is exempt from some contrast requirements when disabled — but don’t rely on disabled as a visual-only state for important content.
- Placeholders often fail contrast — another reason placeholders aren’t labels.
- Error reds on white must still meet ratio for text.
Dark mode
Re-check every pair. Inverting colors isn’t automatic compliance.
Footguns
- Light gray body text on white marketing pages.
- Text over images without scrim.
- Gradient backgrounds with untested midpoints.
- Focus rings that fail non-text contrast.
Interview out-loud answer
“WCAG AA needs 4.5:1 for normal text, 3:1 for large text, and 3:1 for essential UI graphics. I enforce tokens with verified pairs, test dark mode, and use tools rather than eyeballing.”
Text on brand colors
Primary buttons with white text on brand orange/blue often pass; pastel brands fail. Bake contrast checks into the design-token PR checklist. If marketing insists on a light brand fill, darken text or use a darker button variant for UI chrome.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Notes from real codebases
Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.
Related on this site
- Do not rely on color alone
- Focus visible styles
- WCAG principles POUR
- Automated axe testing limits
- Inclusive error messages
Further reading
Related guides
- Accessible Combobox PatternBuild or evaluate comboboxes with APG keyboard behavior, aria-expanded/activedescendant, and filterable listbox pairing.
- Accessible Forms ErrorsLabel inputs, associate errors with fields, announce failures, and avoid placeholder-only forms that break accessibility.
- Accessible Menus PatternAPG menu and menubar keyboard model — arrow navigation, escape to close, aria-expanded, and when a disclosure is enough.
- Accessible Modals PatternsModal dialogs that work: focus trap, Escape, return focus, aria-modal, and why native dialog or APG patterns beat div soup.
- Accessible Names ComputationHow browsers compute accessible names from content, labels, aria-label, and labelledby — and how that powers Testing Library queries.