Contrast and Accessibility
Accessible color choices keep your designs usable for everyone, including the 300+ million people with color vision deficiencies. Not optional. 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 the lighter color
L2 = luminance of the darker color
Luminance ranges from 0 (black) to 1 (white)
You do not need to calculate this by hand. Use a tool (listed below).
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 |
Target AA for everything, AAA for critical content.
Practical Contrast Guidelines
Text on Backgrounds
/* PASS: 4.5:1 or higher for normal text */
.good-contrast {
color: hsl(220, 10%, 20%); /* Dark gray text */
background: hsl(0, 0%, 100%); /* White background */
/* Ratio: ~12:1, PASS */
}
/* 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, FAIL */
}
Text Color Recommendations
For light backgrounds (white or light gray):
:root {
/* Body text: needs 4.5:1 minimum */
--text-primary: hsl(220, 15%, 20%); /* ~13:1 on white, PASS */
/* Secondary text: still needs 4.5:1 */
--text-secondary: hsl(220, 10%, 40%); /* ~6:1 on white, PASS */
/* 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, PASS */
--text-secondary-on-dark: hsl(220, 10%, 70%); /* ~6:1, PASS */
}
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, PASS */
}
/* Focus indicator needs 3:1 against both the element AND the 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, PASS */
}
/* Or 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 pairings are hard for colorblind users:
AVOID: Red + Green (most common confusion)
AVOID: Green + Brown
AVOID: Blue + Purple (tritanopia)
AVOID: Green + Blue (some types)
AVOID: Light green + Yellow
AVOID: Red + Brown
Never Rely on Color Alone
Bad: color is the only signal.
<!-- Bad: color is the only indicator -->
<span class="status-good">Online</span>
<span class="status-bad">Offline</span>
Good: color plus an icon, label, or pattern.
<!-- Good: color + icon + text -->
<span class="status-good">[OK] Online</span>
<span class="status-bad">[X] Offline</span>
<!-- For charts, use different line patterns alongside color -->
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: "[OK] ";
}
Charts and Data Visualization
Do not 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 label data points directly instead of relying on a legend */
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 and Edge):
- DevTools, Rendering tab (More Tools, Rendering)
- Scroll to "Emulate vision deficiencies"
- Select protanopia, deuteranopia, and so on
Design tools:
- Figma: plugins like "Color Blind" or "Stark"
- Sketch: Stark plugin
Test your designs in at least deuteranopia and protanopia. Those are the 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, PASS */
/* For text ON blue-500, we need contrast */
/* White text on blue-500: */
/* hsl(0, 0%, 100%) on hsl(220, 80%, 50%) = 4.6:1, barely passes */
/* Safer: darken the blue for better white text */
--blue-600: hsl(220, 80%, 42%); /* 6.5:1 white text, PASS */
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, PASS */
--primary-600: hsl(220, 85%, 40%); /* 7:1 on white, PASS */
--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 to 200 */
--text-on-primary-dark: hsl(220, 20%, 98%); /* On 500 and darker */
}
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, FAIL */
}
/* Better, but do not depend on placeholders for meaning */
.input::placeholder {
color: hsl(0, 0%, 50%); /* ~5:1 on white, PASS */
}
2. Disabled States Too Faded
/* Bad: you can't tell 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 you remove the 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, style 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 |
WCAG AA is the floor, not the ceiling. Higher contrast is almost always better.
Next: 07-color-in-ui.md covers how to apply color in real interfaces.