Low contrast is the most common accessibility failure on production sites — and the easiest to prevent if you check ratios before ship. Gray #767676 text on white fails for small body copy. Brand primary buttons with light green on white fail for millions of color-blind users. This guide explains WCAG contrast requirements, how to measure them correctly, and how to fix failures without abandoning your palette.
Bottom line: Normal text needs 4.5:1 contrast against its background (WCAG AA). Large text needs 3:1. UI components and graphical objects need 3:1. Check contrast on every state — hover, focus, disabled — not just the default mockup.
What WCAG requires
Web Content Accessibility Guidelines (WCAG) 2.2 define contrast minimum success criteria:
| Criterion |
Level |
Requirement |
| 1.4.3 Contrast (Minimum) |
AA |
4.5:1 normal text, 3:1 large text |
| 1.4.6 Contrast (Enhanced) |
AAA |
7:1 normal text, 4.5:1 large text |
| 1.4.11 Non-text Contrast |
AA |
3:1 for UI components and graphics |
Most legal frameworks reference Level AA as the baseline (EU Accessibility Act, ADA settlements, Section 508).
What counts as "large text"
| Style |
WCAG definition |
| Large |
≥ 18pt (24px) regular or ≥ 14pt (18.66px) bold |
| Normal |
Everything else |
A 16px regular paragraph is normal text — 4.5:1 applies.
How contrast ratio is calculated
Contrast ratio compares relative luminance of foreground and background:
(L1 + 0.05) / (L2 + 0.05)
Where L1 is the lighter color. Range is 1:1 (no contrast) to 21:1 (black on white).
You do not need to compute this manually — use a contrast checker. You do need to know which color pairs to test.
What to test (the full matrix)
Text
- Body copy on page background
- Muted/secondary text on background
- Text on colored cards, badges, banners
- Links in default, hover, visited, focus states
- Placeholder text (should meet contrast or not rely on placeholder alone)
- Error and success messages
Non-text (WCAG 1.4.11)
- Button borders and fills vs adjacent background
- Form input borders vs background
- Icons conveying meaning (not purely decorative)
- Focus rings vs background
- Chart lines and data visualization elements
States teams forget
| State |
Example failure |
| Disabled |
#ccc text on #f5f5f5 — looks "disabled" but unreadable |
| Hover |
Button lightens on hover below 4.5:1 |
| Focus |
Thin #ddd focus ring invisible on white |
| Dark mode |
Passes in light, fails when theme switches |
Test both themes if you ship dark mode.
Worked examples
Failing muted text
/* 3.2:1 — fails AA for body text */
.muted {
color: #999999;
background: #ffffff;
}
Fix options:
- Darken text:
#595959 → ~7:1 on white
- Lighten background if context allows
- Increase font size/weight to qualify as "large" (last resort — do not abuse)
.button {
color: #ffffff;
background: #1a56db; /* ~6.5:1 white on blue */
}
.button:hover {
background: #1e429f; /* maintain or improve ratio */
}
Focus visible
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
Outline must contrast with adjacent colors, not only the element fill.
AA vs AAA: which target?
| Target |
When to use |
| AA |
Legal baseline, most products, public websites |
| AAA |
Health, finance, government, accessibility-first products |
AAA for all body text is hard with soft brand palettes. Common compromise: AA for body, AAA for headings and primary actions where possible.
Color blindness and contrast
Contrast ratio is not the same as color distinguishability. Two colors can pass 4.5:1 but be indistinguishable for deuteranopia users (red/green confusion).
Pair contrast with:
- Icons + text labels (not color alone for errors)
- Patterns or underlines on charts
prefers-color-scheme and forced-colors media queries
Design system workflow
1. Define semantic tokens
:root {
--text-primary: #111827;
--text-secondary: #4b5563; /* verify 4.5:1 on bg */
--text-muted: #6b7280; /* borderline — check size */
--bg-surface: #ffffff;
--interactive-primary: #1d4ed8;
}
2. Document tested pairs in your design system
| Token pair |
Ratio |
WCAG |
Notes |
| text-primary / bg-surface |
16.6:1 |
AAA |
Body |
| text-secondary / bg-surface |
7.0:1 |
AAA |
Captions OK |
| text-muted / bg-surface |
4.6:1 |
AA |
Min for 16px |
3. Gate PRs with automated checks
- ESLint plugin
eslint-plugin-jsx-a11y for obvious issues
- Storybook a11y addon on component stories
- CI screenshot tests with contrast regression (optional)
4. Fix failures systematically
- Identify failing pair
- Adjust foreground first (preserve brand background)
- Re-check all states
- Update Figma tokens to match code
Gradients and images behind text
Contrast applies to the immediate background pixels behind text. Text on a busy photo fails unless you add an overlay:
.hero {
background-image: url("/hero.jpg");
}
.hero-title {
color: #ffffff;
background: rgba(0, 0, 0, 0.55);
padding: 0.25em 0.5em;
box-decoration-break: clone;
}
Test the worst-case region of the image (lightest patch behind white text).
Common excuses (and rebuttals)
| Excuse |
Reality |
| "Brand won't allow darker gray" |
Accessible brand ≠ original hex — adjust hue, keep identity |
| "It's just placeholder text" |
Users still read placeholders |
| "Disabled doesn't need contrast" |
WCAG still requires readable disabled text or alternative cues |
| "We'll fix in v2" |
Lawsuits and lost users happen in v1 |
Use these codefunc tools alongside this guide:
Practical takeaway
Aim for WCAG AA minimum on all shipped UI. Test text and non-text elements. Test hover, focus, and dark mode. Document token pairs in your design system. Fix contrast at the token level — not one-off hotfixes per component.
Sources