Contrast and Accessibility
Accessible color choices ensure your designs work for everyone, including the 300+ million people with color vision deficiencies. This isn't optional - it's essential.
Why Accessibility Matters
- 8% of men and 0.5% of women have color vision deficiency
- Situational impairments affect everyone (bright sunlight, tired eyes, old screens)
- Legal requirements - Many countries mandate accessible digital products
- Better for everyone - High contrast designs are easier for all users
Understanding Contrast Ratio
Contrast ratio measures the difference in luminance between two colors.
The Scale
1:1 ████████████ Same color (no contrast)
3:1 ████████████ Minimum for large text
4.5:1 ████████████ Minimum for normal text (AA)
7:1 ████████████ Enhanced contrast (AAA)
21:1 ████████████ Black on white (maximum)
How It's Calculated
Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
Where L1 = luminance of lighter color
L2 = luminance of darker color
Luminance ranges from 0 (black) to 1 (white)
You don't need to calculate manually - use tools (covered later).
WCAG Requirements
The Web Content Accessibility Guidelines (WCAG) define minimum contrast standards.
Level AA (Minimum - Aim for This)
| Element | Minimum Ratio | Example |
|---|---|---|
| Normal text (<18px or <14px bold) | 4.5:1 | Body copy, labels |
| Large text (≥18px or ≥14px bold) | 3:1 | Headings |
| UI components & graphics | 3:1 | Buttons, icons, form fields |
Level AAA (Enhanced)
| Element | Minimum Ratio |
|---|---|
| Normal text | 7:1 |
| Large text | 4.5:1 |
Recommendation: Target AA for everything, AAA for critical content.
Practical Contrast Guidelines
Text on Backgrounds
/* PASS - 4.5:1+ for normal text */
.good-contrast {
color: hsl(220, 10%, 20%); /* Dark gray text */
background: hsl(0, 0%, 100%); /* White background */
/* Ratio: ~12:1 ✓ */
}
/* FAIL - Below 4.5:1 */
.bad-contrast {
color: hsl(220, 10%, 60%); /* Light gray text */
background: hsl(0, 0%, 100%); /* White background */
/* Ratio: ~3:1 ✗ */
}
Text Color Recommendations
For light backgrounds (white/light gray):
:root {
/* Body text - needs 4.5:1 minimum */
--text-primary: hsl(220, 15%, 20%); /* ~13:1 on white ✓ */
/* Secondary text - still needs 4.5:1 */
--text-secondary: hsl(220, 10%, 40%); /* ~6:1 on white ✓ */
/* Muted/tertiary - large text only (3:1) or decorative */
--text-muted: hsl(220, 10%, 55%); /* ~4:1 on white - borderline */
}
For dark backgrounds:
:root {
--dark-bg: hsl(220, 20%, 12%);
/* Light text on dark - same rules apply */
--text-on-dark: hsl(220, 15%, 90%); /* ~11:1 ✓ */
--text-secondary-on-dark: hsl(220, 10%, 70%); /* ~6:1 ✓ */
}
Interactive Elements
Buttons, links, and form controls need 3:1 against adjacent colors:
/* Button needs 3:1 against background */
.button {
background: hsl(220, 80%, 50%); /* Blue button */
/* Against white page: ~4.5:1 ✓ */
}
/* Focus indicator needs 3:1 against both the element AND background */
.button:focus {
outline: 3px solid hsl(220, 80%, 40%);
outline-offset: 2px;
}
Form Fields
/* Border needs 3:1 against background */
.input {
border: 1px solid hsl(220, 10%, 70%); /* ~2.5:1 on white - FAIL */
}
/* Better */
.input {
border: 1px solid hsl(220, 10%, 55%); /* ~4:1 on white ✓ */
}
/* Or use thicker borders */
.input {
border: 2px solid hsl(220, 10%, 65%); /* Thickness helps visibility */
}
Color Blindness Considerations
Types of Color Vision Deficiency
| Type | Prevalence | Affected Colors |
|---|---|---|
| Deuteranomaly | 5% of males | Green weak |
| Protanomaly | 1% of males | Red weak |
| Deuteranopia | 1% of males | Green blind |
| Protanopia | 1% of males | Red blind |
| Tritanopia | 0.01% | Blue blind (rare) |
| Monochromacy | 0.003% | No color (very rare) |
Problematic Color Combinations
These combinations are difficult for colorblind users:
❌ Red + Green (most common confusion)
❌ Green + Brown
❌ Blue + Purple (tritanopia)
❌ Green + Blue (some types)
❌ Light green + Yellow
❌ Red + Brown
Never Rely on Color Alone
Bad: Using only color to convey meaning
<!-- Bad: Color is the only indicator -->
<span class="status-good">● Online</span>
<span class="status-bad">● Offline</span>
Good: Use color + another indicator (icon, text, pattern)
<!-- Good: Color + icon + text -->
<span class="status-good">✓ Online</span>
<span class="status-bad">✗ Offline</span>
<!-- Good: Color + pattern in charts -->
<!-- Use different line patterns, not just colors -->
Accessible Status Indicators
/* Error: Red + icon + text */
.error {
color: hsl(0, 70%, 40%);
border-left: 4px solid currentColor;
}
.error::before {
content: "⚠ "; /* Icon reinforces meaning */
}
/* Success: Green + icon */
.success {
color: hsl(142, 70%, 30%);
}
.success::before {
content: "✓ ";
}
Charts and Data Visualization
Don't rely on color alone:
/* Use patterns, shapes, or labels */
.chart-line-1 { stroke: hsl(220, 80%, 50%); stroke-dasharray: none; }
.chart-line-2 { stroke: hsl(0, 70%, 50%); stroke-dasharray: 5,5; }
.chart-line-3 { stroke: hsl(120, 60%, 40%); stroke-dasharray: 10,3; }
/* Or use direct labels instead of legends */
Testing for Accessibility
Contrast Checking Tools
Browser Extensions:
- WAVE (Web Accessibility Evaluation Tool)
- axe DevTools
- Accessibility Insights
Online Tools:
- WebAIM Contrast Checker (webaim.org/resources/contrastchecker)
- Coolors Contrast Checker
- Stark (Figma plugin)
DevTools:
- Chrome DevTools: Elements panel → Computed → Contrast ratio
- Firefox: Accessibility Inspector
Color Blindness Simulation
Browser DevTools (Chrome/Edge):
- DevTools → Rendering tab (More Tools → Rendering)
- Scroll to "Emulate vision deficiencies"
- Select protanopia, deuteranopia, etc.
Design Tools:
- Figma: Plugins like "Color Blind" or "Stark"
- Sketch: Stark plugin
Test your designs in at least deuteranopia and protanopia (most common).
Building Accessible Color Palettes
Process
- Start with your brand colors
- Check contrast against white and black
- Adjust lightness to meet requirements
- Create a light and dark variant for each
- Test with colorblind simulators
Example: Creating an Accessible Blue
/* Starting color */
--blue-500: hsl(220, 80%, 50%); /* 4.5:1 on white ✓ */
/* For use ON blue-500, we need text that contrasts */
/* White text on blue-500: */
/* hsl(0, 0%, 100%) on hsl(220, 80%, 50%) = 4.6:1 ✓ barely */
/* Safer: darken the blue for better white text */
--blue-600: hsl(220, 80%, 42%); /* 6.5:1 white text ✓ */
Semantic Color Scale Example
:root {
/* Primary - tested for contrast */
--primary-50: hsl(220, 80%, 97%); /* Backgrounds */
--primary-100: hsl(220, 75%, 92%);
--primary-200: hsl(220, 70%, 85%);
--primary-300: hsl(220, 65%, 70%);
--primary-400: hsl(220, 70%, 55%);
--primary-500: hsl(220, 80%, 50%); /* 4.5:1 on white ✓ */
--primary-600: hsl(220, 85%, 40%); /* 7:1 on white ✓ */
--primary-700: hsl(220, 90%, 32%);
--primary-800: hsl(220, 90%, 24%);
--primary-900: hsl(220, 90%, 16%);
/* Text colors */
--text-on-primary-light: var(--primary-900); /* On 50-200 */
--text-on-primary-dark: hsl(220, 20%, 98%); /* On 500+ */
}
Common Accessibility Mistakes
1. Placeholder Text with Low Contrast
/* Bad - Placeholder is too light */
.input::placeholder {
color: hsl(0, 0%, 75%); /* ~2:1 on white ✗ */
}
/* Better - But placeholders shouldn't be relied on */
.input::placeholder {
color: hsl(0, 0%, 50%); /* ~5:1 on white ✓ */
}
2. Disabled States Too Faded
/* Bad - Can't see it's a button */
.button:disabled {
opacity: 0.3;
}
/* Better - Still visible, clearly disabled */
.button:disabled {
background: hsl(220, 10%, 85%);
color: hsl(220, 10%, 55%);
cursor: not-allowed;
}
3. Links Not Distinguishable
/* Bad - Links look like regular text */
a {
color: inherit;
text-decoration: none;
}
/* Better - Clear link indication */
a {
color: hsl(220, 80%, 45%);
text-decoration: underline;
}
/* Or if removing underline, use another indicator */
a {
color: hsl(220, 80%, 45%);
text-decoration: none;
border-bottom: 1px solid currentColor;
}
4. Focus States Removed
/* NEVER do this */
*:focus {
outline: none; /* Breaks keyboard navigation */
}
/* Instead, customize it */
*:focus-visible {
outline: 2px solid hsl(220, 80%, 50%);
outline-offset: 2px;
}
Quick Reference: Contrast Targets
| Element | Min Ratio | Target |
|---|---|---|
| Body text | 4.5:1 | 7:1+ |
| Large text (18px+) | 3:1 | 4.5:1+ |
| Placeholder text | 4.5:1 | - |
| Links | 3:1 vs surrounding text | Underline too |
| Buttons | 3:1 vs background | 4.5:1+ |
| Form borders | 3:1 vs background | - |
| Focus indicators | 3:1 vs adjacent | - |
| Icons (meaningful) | 3:1 | - |
| Disabled elements | No minimum | Make visually distinct |
Summary
| Rule | Implementation |
|---|---|
| Body text: 4.5:1 | Dark text ~20% lightness on white |
| Large text: 3:1 | Slightly more flexibility |
| UI elements: 3:1 | Buttons, inputs, icons |
| Never color alone | Add icons, text, patterns |
| Test colorblind | DevTools → Emulate vision deficiencies |
| Test contrast | WebAIM, DevTools, browser extensions |
The accessibility floor, not ceiling: Meeting WCAG AA is the minimum. Higher contrast is almost always better.
Next: 07-color-in-ui.md - Applying color to user interfaces