Accessibility
Color Contrast
Ensure readable text for all users
Beginner
a11y design wcag color
Definition
Color contrast refers to the difference in luminance between foreground (text) and background colors. Sufficient contrast ensures text is readable by users with visual impairments, including color blindness, and improves readability for everyone, especially in bright sunlight or on low-quality displays.
WCAG Contrast Requirements
Contrast Ratios
Normal Text:
- AA: 4.5:1 minimum
- AAA: 7:1 minimum
Large Text (18pt+ or 14pt+ bold):
- AA: 3:1 minimum
- AAA: 4.5:1 minimum
UI Components/Graphics:
- AA: 3:1 minimum
Calculating Contrast
// Relative luminance formula
function getLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(val => {
val = val / 255;
return val <= 0.03928
? val / 12.92
: Math.pow((val + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(color1, color2) {
const lum1 = getLuminance(...color1);
const lum2 = getLuminance(...color2);
const brightest = Math.max(lum1, lum2);
const darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
// Example: White on Blue
const white = [255, 255, 255];
const blue = [0, 100, 200];
const ratio = getContrastRatio(white, blue); // 4.6:1 (passes AA)
Testing Contrast
Browser DevTools
/* Chrome DevTools shows contrast ratio */
.my-text {
color: #666666; /* DevTools shows: Contrast ratio 5.74:1 ✓ */
}
Automated Testing
// Using axe-core
const axe = require('axe-core');
const results = await axe.run();
const contrastViolations = results.violations.filter(
v => v.id === 'color-contrast'
);
Visual Testing
/* Simulate color blindness */
/* Chrome DevTools: Rendering > Emulate vision deficiencies */
.protanopia { filter: url('#protanopia'); }
.deuteranopia { filter: url('#deuteranopia'); }
.tritanopia { filter: url('#tritanopia'); }
.achromatopsia { filter: grayscale(100%); }
Common Issues
Low Contrast Text
/* ❌ Too light */
.text-light {
color: #cccccc; /* 1.6:1 on white - FAILS */
}
/* ✅ Good contrast */
.text-good {
color: #595959; /* 7:1 on white - PASSES AAA */
}
Placeholder Text
/* ❌ Placeholders often too light */
input::placeholder {
color: #999999; /* Often fails contrast */
}
/* ✅ Darker placeholder */
input::placeholder {
color: #666666; /* 5.74:1 on white */
}
Links Without Underlines
/* ❌ Color-only differentiation */
a {
color: #0066cc;
text-decoration: none; /* Hard to see if color blind */
}
/* ✅ Underline or sufficient contrast */
a {
color: #0052a3; /* Darker blue */
text-decoration: underline;
}
/* Or */
a {
color: #0066cc;
text-decoration: none;
border-bottom: 1px solid currentColor;
}
Best Practices
Don’t Rely on Color Alone
/* ❌ Color-only error indication */
.error {
color: #ff0000;
}
/* ✅ Add icon and text */
.error {
color: #d32f2f;
}
.error::before {
content: "⚠️ ";
}
<!-- HTML: <span class="error">⚠️ Error: Invalid input</span> -->
Focus Indicators
/* ❌ No visible focus */
button:focus {
outline: none;
}
/* ✅ Visible focus with sufficient contrast */
button:focus {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
Dark Mode Considerations
:root {
--text-primary: #212121;
--bg-primary: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--text-primary: #e0e0e0;
--bg-primary: #121212;
}
}
body {
color: var(--text-primary);
background: var(--bg-primary);
}
Tools
Online Checkers
- WebAIM Contrast Checker
- Colour Contrast Analyser (CCA)
- Stark (Figma/Sketch plugin)
- axe DevTools
CSS Custom Properties for Themes
:root {
/* Ensure all combinations meet 4.5:1 */
--color-text: #212121;
--color-text-secondary: #616161;
--color-bg: #ffffff;
--color-bg-secondary: #f5f5f5;
--color-primary: #1565c0;
--color-error: #c62828;
}
Key Takeaway
Maintain minimum 4.5:1 contrast for normal text and 3:1 for large text. Never rely on color alone to convey information. Test with automated tools and simulate color blindness. Good contrast benefits all users, not just those with visual impairments.