WCAG Color Contrast Guide: Accessible Web Design
Master WCAG color contrast requirements for accessible web design. Understand contrast ratios, AA vs AAA standards, how to test your designs, and practical tips for passing accessibility audits.
Color contrast is one of the most common accessibility failures on the web—and one of the most preventable. An estimated 1 in 12 men and 1 in 200 women have some form of color vision deficiency. For users with low vision or aging eyes, poor contrast doesn’t just make text hard to read; it makes it invisible.
WCAG (Web Content Accessibility Guidelines) defines specific contrast ratios that text and UI elements must meet. Getting this right is no longer optional—it’s a legal requirement in many jurisdictions and a core part of building software that actually works for everyone.
This guide explains the math behind contrast ratios, what WCAG requires, how to test your designs, and practical strategies for building accessible color systems. Use the DevPlaybook Color Picker to experiment with colors as you read.
What Is Color Contrast Ratio?
Contrast ratio is a mathematical expression of the luminance difference between two colors. It’s calculated using the relative luminance of each color as defined in the WCAG 2.x specification.
Relative Luminance Formula
Relative luminance represents a color’s perceived brightness on a scale from 0 (absolute black) to 1 (absolute white).
For each RGB channel (normalized to 0–1):
if channel <= 0.04045:
C_linear = channel / 12.92
else:
C_linear = ((channel + 0.055) / 1.055) ^ 2.4
Then luminance:
L = 0.2126 * R_linear + 0.7152 * G_linear + 0.0722 * B_linear
Contrast Ratio Formula
contrast ratio = (L1 + 0.05) / (L2 + 0.05)
Where L1 is the lighter color’s luminance and L2 is the darker color’s luminance. The result is always ≥ 1:1. White on white = 1:1. Black on white = 21:1 (maximum possible ratio).
In JavaScript:
function getLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
const sRGB = c / 255;
return sRGB <= 0.04045
? sRGB / 12.92
: Math.pow((sRGB + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(rgb1, rgb2) {
const L1 = getLuminance(...rgb1);
const L2 = getLuminance(...rgb2);
const lighter = Math.max(L1, L2);
const darker = Math.min(L1, L2);
return (lighter + 0.05) / (darker + 0.05);
}
// Example
const ratio = getContrastRatio([0, 102, 204], [255, 255, 255]);
// #0066CC on white = ~4.56:1
WCAG Contrast Requirements
WCAG 2.1 and 2.2 define contrast requirements across two conformance levels: AA (the minimum for most legal and practical requirements) and AAA (enhanced accessibility).
Text Contrast (Success Criterion 1.4.3 — Level AA)
| Text Type | Minimum Ratio (AA) | Enhanced Ratio (AAA) |
|---|---|---|
| Normal text (< 18pt or < 14pt bold) | 4.5:1 | 7:1 |
| Large text (≥ 18pt or ≥ 14pt bold) | 3:1 | 4.5:1 |
| Text in logos or decorative text | No requirement | No requirement |
| Disabled text | No requirement | No requirement |
In CSS terms:
- Normal text: less than
24pxregular weight, or less than18.67pxbold - Large text:
24pxor larger at normal weight, or18.67pxor larger at bold
Non-Text Contrast (Success Criterion 1.4.11 — Level AA)
Since WCAG 2.1, UI components and graphical objects must also meet contrast requirements:
| Element | Minimum Ratio |
|---|---|
| Form inputs (border vs. background) | 3:1 |
| Focus indicators | 3:1 |
| Icons (when essential to understanding) | 3:1 |
| Chart elements (lines, bars, segments) | 3:1 |
| State indicators (checked checkbox, active tab) | 3:1 |
Non-text contrast at 3:1 is lower than text because these elements convey structure or state rather than linguistic meaning.
WCAG 2.2 Updates
WCAG 2.2 (2023) added new requirements including:
- Focus Visible (2.4.11 AA): Focus indicators must have a minimum area and contrast ratio. Specifically, the focus indicator must be at least as large as a 2px perimeter and have a 3:1 contrast ratio against adjacent colors.
- Focus Not Obscured (2.4.12 AA): Focused components must not be entirely hidden by sticky headers or overlays.
These requirements affect how you style :focus-visible in CSS.
Common Failures (with Fixes)
Failure 1: Gray Text on White Background
/* Fails: #767676 on white = 4.48:1 (just below AA for normal text) */
.body-text {
color: #767676;
background: #ffffff;
}
/* Pass: #696969 on white = 5.07:1 */
.body-text {
color: #696969;
background: #ffffff;
}
/* Or use the slightly darker WCAG-compliant minimum */
.body-text {
color: #757575; /* 4.6:1 on white — passes AA */
background: #ffffff;
}
The exact minimum gray that passes WCAG AA on white is #767676 — but only barely (4.48:1 rounds to a fail in strict checkers). Use #757575 or darker for safety.
Failure 2: Blue Text on Blue Background
/* Fails: #4A90E2 on #2C5282 = 1.9:1 */
.badge {
color: #4A90E2;
background: #2C5282;
}
/* Pass: white on #2C5282 = 7.5:1 */
.badge {
color: #ffffff;
background: #2C5282;
}
Avoid using similar hue/saturation combinations. When in doubt, use white or black text.
Failure 3: Placeholder Text
/* Fails: #9CA3AF placeholder on white = 2.9:1 */
input::placeholder {
color: #9CA3AF;
}
/* Pass: use slightly darker placeholder */
input::placeholder {
color: #6B7280; /* 5.74:1 on white */
}
Placeholder text is technically not “required” content, but WCAG 2.1 clarified that if placeholder text conveys essential information, it must meet contrast requirements. Best practice: darken your placeholders.
Failure 4: Low-Contrast Focus Rings
/* Fails: thin blue border can be invisible on some backgrounds */
:focus {
outline: 1px solid #4A90E2;
}
/* Pass: WCAG 2.2 compliant focus style */
:focus-visible {
outline: 2px solid #0066CC;
outline-offset: 2px;
/* Verify #0066CC has 3:1 contrast against adjacent background */
}
Failure 5: White Text on Light Background in Dark Mode
/* Works in light mode but fails in dark mode if misconfigured */
.card {
background: #1a1a2e;
color: #ffffff; /* 15.5:1 — fine */
}
/* Subtle dark mode failure */
.muted {
background: #2d2d2d;
color: #9d9d9d; /* 3.0:1 — fails AA for normal text */
}
Dark mode is not automatically accessible. Recalculate contrast ratios for every color combination in your dark theme.
Building an Accessible Color System
Rather than checking colors individually, build a system where contrast is predictable.
The “10 Shades” Approach
Design your palette as a 10-step scale from 50 (lightest) to 900 (darkest). Establish rules:
| Text Color | Background Constraint |
|---|---|
| Shade 700+ | Always safe on white or shade 50–100 |
| Shade 600 | Check against specific backgrounds |
| Shade 500 | Use for large text or UI elements only |
| Shade 100– | Usually safe on shade 800+ |
/* Example: blue palette where 700+ passes on white */
:root {
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-200: #bfdbfe;
--blue-300: #93c5fd; /* 2.6:1 on white — decorative only */
--blue-400: #60a5fa; /* 3.0:1 on white — large text only */
--blue-500: #3b82f6; /* 4.0:1 on white — close to AA */
--blue-600: #2563eb; /* 5.9:1 on white — passes AA */
--blue-700: #1d4ed8; /* 7.8:1 on white — passes AAA */
--blue-800: #1e40af; /* 10.1:1 on white */
--blue-900: #1e3a8a; /* 13.0:1 on white */
}
In this system, --blue-600 and above are safe for normal text on white.
Semantic Color Tokens
Rather than exposing raw hex values, use semantic tokens that encode accessibility intent:
:root {
/* Text colors — all verified against white background */
--text-primary: #111827; /* 18.1:1 */
--text-secondary: #374151; /* 11.0:1 */
--text-muted: #6B7280; /* 5.74:1 — AA only */
--text-disabled: #9CA3AF; /* 2.9:1 — use only for disabled state */
/* Interactive colors */
--link-color: #1D4ED8; /* 7.8:1 on white */
--link-hover: #1E3A8A; /* 13.0:1 on white */
/* Focus ring */
--focus-ring: #0066CC; /* 4.56:1 on white */
}
Testing Color Contrast
Automated Testing
In code (during build/test):
# axe-core (most comprehensive)
npm install axe-core
# In Jest/Vitest
import { axe } from 'jest-axe';
const results = await axe(container);
expect(results).toHaveNoViolations();
CI/CD integration:
# pa11y — command-line accessibility checker
npx pa11y https://yoursite.com --standard WCAG2AA
# pa11y in CI
npx pa11y-ci --config .pa11yci.json
// .pa11yci.json
{
"defaults": {
"standard": "WCAG2AA",
"runners": ["axe"]
},
"urls": [
"http://localhost:3000",
"http://localhost:3000/about"
]
}
Browser DevTools
Chrome:
- Open DevTools → Elements → Computed
- Click a text element
- Under “Color”, DevTools shows contrast ratio with a pass/fail indicator
Firefox:
- Open DevTools → Accessibility
- Enable “Show Tabbing Order” to visualize focus flow
- The Accessibility Inspector shows contrast ratios per element
Design Tools
Figma:
- Plugin: Contrast by Figma (free)
- Plugin: A11y - Color Contrast Checker
- Use the built-in Color Blindness Simulation to preview your design across 8 types of color vision
Storybook:
npm install @storybook/addon-a11y
Add to .storybook/main.js:
export default {
addons: ['@storybook/addon-a11y']
};
This runs axe-core on each story and shows violations in the “Accessibility” panel.
Color Vision Deficiency: What Users Actually See
Understanding the types of color blindness helps you design better, not just pass a ratio check.
| Type | Prevalence | What’s affected |
|---|---|---|
| Deuteranopia (red-green) | ~5% of men | Green deficiency—reds/greens look similar |
| Protanopia (red-green) | ~1% of men | Red deficiency—reds appear dark |
| Tritanopia (blue-yellow) | <0.01% | Blue-yellow confusion |
| Achromatopsia | Very rare | No color vision—sees only luminance |
Key insight: Contrast ratio is based on luminance, not hue. This is why a red (#CC0000) and green (#008800) can have poor contrast even though they “look different” to most people—their luminance values are similar.
// Red vs green — perceived as very different, but luminance is close
getContrastRatio([204, 0, 0], [0, 136, 0]) // ≈ 1.3:1 — terrible contrast
Never rely on color alone to convey information. Always pair color with:
- Text labels
- Icons
- Patterns or shapes
- Position
WCAG 3.0 Preview: APCA
The upcoming WCAG 3.0 uses a new algorithm called APCA (Advanced Perceptual Contrast Algorithm). APCA:
- Accounts for text size and weight in the calculation (not just as thresholds)
- Uses a perceptual lightness model more aligned with human vision (Lc scale)
- Handles dark backgrounds differently from light backgrounds (contrast is not symmetric)
- Produces scores from Lc 0 to Lc 106 rather than ratios
APCA is currently in beta as part of the WCAG 3 working draft. If you want to future-proof:
- Aim for Lc 60+ for body text (stricter than WCAG 2.x in some cases)
- Use the APCA Contrast Calculator to preview scores
- The current WCAG 2.x requirements remain legally binding—APCA is additive, not a replacement yet
Quick Reference: Safe Text Colors on Common Backgrounds
| Background | Minimum Safe Dark Text | Minimum Safe Light Text |
|---|---|---|
| White (#fff) | #767676 (barely—use #757575) | n/a |
| Light gray (#f3f4f6) | #6B7280 | n/a |
| Medium blue (#2563EB) | n/a | #ffffff |
| Dark gray (#1F2937) | n/a | #9CA3AF (large text only) |
| Dark navy (#1E3A8A) | n/a | #ffffff |
Summary
Accessible color contrast comes down to three practical habits:
- Build a verified color scale — establish which shades are safe for text vs. decoration before writing any CSS
- Test in context — automated tools catch most issues; supplement with manual DevTools checks for interactive states (hover, focus, disabled)
- Never rely on color alone — pair color cues with text, icons, or other non-color signals
The DevPlaybook Color Picker lets you explore colors and verify RGB/hex values as you design. For full contrast ratio calculations, paste your hex values into any of the checker tools above.
The 4.5:1 ratio for normal text and 3:1 for large text and UI components are the numbers to know. Everything else in this guide is context for meeting them correctly.
Working with accessibility in CSS? The DevPlaybook CSS Flexbox Guide shows how to build layout patterns that work correctly with screen readers. For consistent design tokens, explore the Color Picker to build your accessible palette.
Free Newsletter
Level Up Your Dev Workflow
Get new tools, guides, and productivity tips delivered to your inbox.
Plus: grab the free Developer Productivity Checklist when you subscribe.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links — we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.