Text color contrast must meet WCAG ratios
Last updated:
Related Guides
Text color contrast must meet WCAG ratios without exceptions
All visible text must have enough contrast with its background. This applies to body copy, links, buttons, and text inside images. Low contrast harms people with low vision and color-vision deficiencies, and it also reduces readability in glare or poor lighting.
Why It Matters
Low contrast makes letters blend into the background. Users with low vision, color-vision deficiency, or age-related vision loss may be unable to read content, even when zoomed. Poor contrast also increases cognitive effort for everyone, especially on mobile in bright light.
Common Causes
- Pale text on light backgrounds or dark gray on black.
- Text placed over photos, gradients, or video without a solid backing.
- Disabled states, placeholders, and helper text styled lighter than minimums.
- Link colors that are too light, especially when visited/hovered.
- Semi-transparent overlays that reduce effective contrast.
- Image-of-text (e.g., banners) not designed with sufficient contrast.
How to Fix
- Identify text sizes and weights:
- Normal text: smaller than large-scale.
- Large-scale text: at least 24 CSS px regular, or 19 CSS px bold (≈18pt regular, 14pt bold).
- Apply WCAG 2.2 contrast ratios for text:
- AA minimum: 4.5:1 for normal text; 3:1 for large-scale text.
- AAA enhanced (recommended for critical UI and small text): 7:1 for normal text; 4.5:1 for large-scale text.
- Logos and purely decorative text are exempt, but treat marketing text as real text.
- Choose compliant colors:
- Test candidate foreground/background pairs with a contrast checker.
- Prefer solid backgrounds behind text. If using imagery, add a solid or sufficiently opaque overlay to maintain the ratio.
- Style interactive states:
- Ensure default, hover, focus, active, and visited states all meet the required ratios.
- Do not rely on color alone to denote links; ensure contrast and add underline by default or on focus/hover.
- Fix special cases:
- Placeholders and helper text must meet the same ratios as other text.
- Disabled text should still be readable; if interaction must look disabled, reduce contrast cautiously or provide alternative cues (e.g., opacity with adequate text contrast, lock icon) while meeting minimums for essential information.
- Document and enforce:
- Add color tokens with known ratios in your design system.
- Lint CSS or use design plugins that flag noncompliant pairs.
How to Test
Keyboard check
- Tab through interactive text (links, buttons). Confirm each state preserves text contrast against its background.
- Ensure focusable text remains readable when focused.
Screen reader check
- Verify that text is actual text (not inaccessible image-of-text). If images contain text, confirm an accessible text alternative exists and that the visual text meets contrast.
Mobile/touch check
- Test in bright light and Dark Mode/High Contrast themes, if supported. Confirm color tokens keep ratios in each theme.
Quick checklist
- Use a contrast analyzer to verify:
- Normal text ≥ 4.5:1 (AA) or ≥ 7:1 (AAA).
- Large-scale text ≥ 3:1 (AA) or ≥ 4.5:1 (AAA).
- All interactive states meet the same thresholds.
- Text over images/gradients maintains the ratio at every point.
- Placeholders, helper text, and disabled states remain readable.
Good Example
<html>
<head>
<style>
:root {
/* AA-safe pairs on light background */
--bg: #ffffff; /* white */
--text: #1a1a1a; /* 21:1 on white */
--muted: #4a5568; /* ~9:1 on white */
--link: #0a66c2; /* ~6.4:1 on white */
--link-visited: #6f42c1; /* ~6.2:1 on white */
}
body { background: var(--bg); color: var(--text); font: 16px1.5 system-ui, sans-serif; }
a { color: var(--link); text-decoration: underline; }
a:visited { color: var(--link-visited); }
.hero {
/* Text over image with solid overlay to preserve contrast */
position: relative; color: #ffffff; height: 240px;background: url('photo.jpg') center/cover no-repeat;
}
.hero::before { content: ""; position: absolute; inset: 0; background: rgba(0,0,0,0.55); }
.hero h1 { position: relative; font-size: 32px; font-weight: 700; }
.helper { color: var(--muted); }
</style>
</head>
<body>
<h1>Readable headlines with strong contrast</h1>
<p>Body text meets at least 4.5:1 on a white background.</p>
<p class="helper">Helper text also exceeds minimum contrast.</p>
<p><a href="#">Learn more about our service</a></p>
<section class="hero">
<h1>Text over images kept readable</h1>
</section>
</body>
</html>
Bad Example
<html>
<head>
<style>
body { background: #f8f9fb; color: #9aa0a6; font: 14px1.4 Arial, sans-serif; }
a { color: #9bbce0; text-decoration: none; } /* low contrast and no underline */
.banner { position: relative; color: #dddddd; height: 220px; background: url('busy.jpg') centercover; }
.banner h2 { font-size: 18px; font-weight: 400; } /* not large-scale and very low contrast */
.placeholder { color: #c5cbd3; }
</style>
</head>
<body>
<h1>Light gray on light background</h1>
<p class="placeholder">Enter your email</p>
<p><a href="#">More info</a></p>
<section class="banner">
<h2>Offer ends soon</h2>
</section>
</body>
</html>Quick Checklist
- Normal text ≥ 4.5:1 (AA); large-scale text ≥ 3:1 (AA).
- Aim for AAA where feasible: 7:1 normal; 4.5:1 large-scale.
- Verify all states: default, hover, focus, active, visited.
- Avoid text directly on busy images; add a solid or opaque overlay.
- Ensure placeholders, helper text, and disabled states stay readable.
- Use color tokens with documented ratios across themes.
- Re-test after theme changes (Dark Mode, High Contrast) and content updates.
- Treat image-of-text as real text for contrast, or provide accessible text alternatives.