How to Fix Color Contrast Issues: WCAG 2.2 Remediation Tutorial
TABLE OF CONTENTS
- Understanding Contrast Requirements
- Step 1: Identify Contrast Issues
- Step 2: Calculate Contrast Ratios
- Step 3: Fix Text Contrast
- Step 4: Fix Link Contrast
- Step 5: Fix Form Element Contrast
- Step 6: Fix Button Contrast
- Step 7: Fix Images with Text
- Step 8: Fix Icon Contrast
- Step 9: Implement Dark Mode Correctly
- Step 10: Verify Fixes
- Quick Reference: Safe Color Combinations
- Taking Action
- Related Resources
Color contrast failures are among the most common accessibility violations. Studies consistently show that insufficient contrast affects over 80% of websites. Fortunately, contrast issues are also among the easiest to fix. This tutorial walks through identifying contrast problems and implementing solutions that meet WCAG requirements.
Understanding Contrast Requirements
WCAG Contrast Ratios
WCAG 2.2 specifies minimum contrast ratios between text and background:
Level AA (Standard requirement):
- Normal text: 4.5:1 minimum
- Large text (18pt+ or 14pt+ bold): 3:1 minimum
- Non-text elements (icons, borders): 3:1 minimum
Level AAA (Enhanced):
- Normal text: 7:1 minimum
- Large text: 4.5:1 minimum
What Counts as Large Text
Regular weight (400): 18pt (24px) or larger
Bold weight (700+): 14pt (18.5px) or larger
Non-Text Contrast (WCAG 2.1+)
User interface components and graphics require 3:1 contrast:
- Form field borders
- Icons conveying information
- Focus indicators
- Chart elements
Step 1: Identify Contrast Issues
Automated Testing
Start with automated tools to find contrast violations:
Browser DevTools:
// Chrome DevTools - Inspect element
// View "Accessibility" panel for contrast info
// Or use Coverage panel:
// 1. Open DevTools (F12)
// 2. Elements panel > Inspect element
// 3. View computed styles > color
// 4. See contrast ratio displayedCommand line testing:
# Using pa11y
npx pa11y https://example.com --include-warnings
# Output includes contrast violations:
# • Error: WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.FailOnline tools:
- WebAIM Contrast Checker
- Colour Contrast Analyser (desktop app)
- axe DevTools browser extension
Manual Verification
Check areas automated tools miss:
Images with text overlays:
<!-- Check contrast between text and all underlying colors -->
<div class="hero" style="background-image: url(hero.jpg)">
<h1>Welcome</h1> <!-- Is this readable against the image? -->
</div>Dynamic states:
- Hover states
- Focus states
- Selected/active states
- Disabled states
- Error states
Step 2: Calculate Contrast Ratios
Understanding the Formula
Contrast ratio is calculated using relative luminance:
Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
Where L1 = lighter color luminance
L2 = darker color luminanceUsing Tools
Browser DevTools (Chrome):
- Inspect element
- Click the color swatch in Styles panel
- View "Contrast ratio" in color picker
- See if it passes AA/AAA
WebAIM Contrast Checker:
- Enter foreground color (hex, RGB, or HSL)
- Enter background color
- View pass/fail for different text sizes
Finding colors that pass: Most tools show you what contrast ratio you need and suggest fixes.
Step 3: Fix Text Contrast
Adjusting Text Color
Before (failing):
/* Gray text on white - 2.5:1 ratio (FAIL) */
.light-text {
color: #999999;
background: #ffffff;
}After (passing):
/* Darker gray on white - 4.6:1 ratio (PASS AA) */
.light-text {
color: #767676;
background: #ffffff;
}
/* Even darker for AAA - 7:1 ratio */
.light-text-aaa {
color: #595959;
background: #ffffff;
}Adjusting Background Color
Sometimes darkening the background is better than changing text:
Before (failing):
/* White text on light blue - 2.8:1 ratio (FAIL) */
.button {
color: #ffffff;
background: #5BC0DE;
}After (passing):
/* White text on darker blue - 4.5:1 ratio (PASS) */
.button {
color: #ffffff;
background: #0077B6;
}Maintaining Brand Colors
When brand colors fail contrast:
Option 1: Adjust the shade
/* Original brand blue: #1E90FF (fails at 3.0:1) */
/* Darkened version: #0066CC (passes at 4.5:1) */
:root {
--brand-blue: #0066CC; /* Accessible version */
--brand-blue-light: #1E90FF; /* Use only for decorative/large */
}
.text-content {
color: var(--brand-blue);
}
.large-heading {
color: var(--brand-blue-light); /* OK for 24px+ */
}Option 2: Use for non-text only
/* Reserve failing colors for elements without contrast requirements */
.decorative-border {
border-color: #1E90FF; /* Decorative only */
}Step 4: Fix Link Contrast
Links Must Be Distinguishable
Links need 3:1 contrast against surrounding text (or use underlines):
Using underlines (recommended):
a {
color: #0066CC;
text-decoration: underline;
}
a:hover,
a:focus {
color: #004499;
text-decoration: none;
}Without underlines (requires 3:1 vs surrounding text):
/* Body text is #333333 */
/* Link must have 3:1 contrast against #333333 */
a {
color: #0066CC; /* 3:1 against #333 */
text-decoration: none;
}
/* Additional visual cue on focus/hover */
a:hover,
a:focus {
text-decoration: underline;
}Step 5: Fix Form Element Contrast
Input Field Borders
/* Before: Light gray border - 1.5:1 ratio (FAIL) */
input {
border: 1px solid #CCCCCC;
}
/* After: Darker border - 3:1 ratio (PASS) */
input {
border: 1px solid #767676;
}
/* Focus state needs visible change */
input:focus {
border-color: #0066CC;
outline: 2px solid #0066CC;
outline-offset: 2px;
}Placeholder Text
/* Before: Light placeholder - 2.3:1 ratio (FAIL) */
input::placeholder {
color: #AAAAAA;
}
/* After: Darker placeholder - 4.5:1 ratio (PASS) */
input::placeholder {
color: #767676;
}Error States
/* Error styling with sufficient contrast */
.input-error {
border-color: #D32F2F; /* Red with good contrast */
background-color: #FFEBEE;
}
.error-message {
color: #B71C1C; /* Dark red - 5.9:1 on white */
}Step 6: Fix Button Contrast
Primary Buttons
/* Button text and background */
.button-primary {
color: #FFFFFF;
background-color: #0066CC;
/* White on #0066CC = 4.5:1 (PASS) */
}
/* Hover state - still accessible */
.button-primary:hover {
background-color: #004499;
/* White on #004499 = 7.9:1 (PASS AAA) */
}
/* Focus state - visible indicator */
.button-primary:focus {
outline: 3px solid #004499;
outline-offset: 2px;
}
/* Disabled state */
.button-primary:disabled {
background-color: #6699CC;
/* Indicate disabled differently than contrast */
cursor: not-allowed;
opacity: 0.6;
}Secondary/Outline Buttons
.button-secondary {
color: #0066CC;
background-color: transparent;
border: 2px solid #0066CC;
/* Text and border both need 3:1 vs background */
}
.button-secondary:hover {
background-color: #E6F0FF;
/* Ensure text still readable on hover bg */
}Step 7: Fix Images with Text
Text Over Images
Option 1: Semi-transparent overlay
.hero {
position: relative;
background-image: url(hero.jpg);
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6); /* Dark overlay */
}
.hero h1 {
position: relative;
color: #FFFFFF;
/* White text now contrasts against dark overlay */
}Option 2: Solid background behind text
.hero h1 {
background-color: rgba(0, 0, 0, 0.8);
padding: 1rem;
color: #FFFFFF;
}Option 3: Text shadow (less reliable)
/* Use with caution - doesn't guarantee contrast */
.hero h1 {
color: #FFFFFF;
text-shadow:
2px 2px 4px rgba(0, 0, 0, 0.8),
-2px -2px 4px rgba(0, 0, 0, 0.8);
}Step 8: Fix Icon Contrast
Informative Icons
Icons that convey meaning need 3:1 contrast:
/* Status icons */
.icon-success {
color: #2E7D32; /* Green with 4.5:1 on white */
}
.icon-error {
color: #C62828; /* Red with 5.6:1 on white */
}
.icon-warning {
color: #E65100; /* Orange with 3.4:1 on white */
}
/* If icons alone convey status, add text alternatives */<!-- Icon with text for clarity -->
<span class="status">
<svg class="icon-success" aria-hidden="true">...</svg>
<span>Complete</span>
</span>
<!-- Icon-only needs accessible name -->
<button aria-label="Delete item">
<svg class="icon-delete" aria-hidden="true">...</svg>
</button>Step 9: Implement Dark Mode Correctly
CSS Variables for Themes
:root {
/* Light theme (default) */
--text-primary: #1A1A1A; /* 14.5:1 on white */
--text-secondary: #595959; /* 7:1 on white */
--bg-primary: #FFFFFF;
--bg-secondary: #F5F5F5;
--link-color: #0066CC; /* 4.5:1 on white */
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark theme - all ratios recalculated */
--text-primary: #FFFFFF; /* 16:1 on dark bg */
--text-secondary: #B3B3B3; /* 7.4:1 on dark bg */
--bg-primary: #121212;
--bg-secondary: #1E1E1E;
--link-color: #6DB3F2; /* 5.2:1 on dark bg */
}
}
body {
color: var(--text-primary);
background-color: var(--bg-primary);
}
a {
color: var(--link-color);
}Testing Both Themes
Always verify contrast in both light and dark modes—colors that pass in one may fail in the other.
Step 10: Verify Fixes
Re-test After Changes
# Run automated tests again
npx pa11y https://example.com
# Compare before/after
# All contrast violations should be resolvedCross-Browser Testing
Contrast can render differently across browsers and operating systems. Test on:
- Chrome, Firefox, Safari, Edge
- Windows, macOS, iOS, Android
- Different display settings
Document Changes
Track contrast fixes for design system:
/* Design System - Accessible Color Palette
*
* All colors verified for WCAG 2.1 AA contrast
*
* Text colors (on white #FFFFFF):
* - Primary: #1A1A1A (14.5:1)
* - Secondary: #595959 (7.0:1)
* - Tertiary: #767676 (4.5:1)
*
* Link colors (on white #FFFFFF):
* - Default: #0066CC (4.5:1)
* - Hover: #004499 (7.9:1)
*
* UI colors (3:1 minimum for non-text):
* - Border: #767676 (4.5:1)
* - Icon: #595959 (7.0:1)
*/Quick Reference: Safe Color Combinations
On White Background (#FFFFFF)
| Text Color | Hex | Ratio | Passes |
|--------------|---------|--------|--------|
| Black | #000000 | 21:1 | AAA |
| Dark Gray | #333333 | 12.6:1 | AAA |
| Medium Gray | #595959 | 7.0:1 | AAA |
| Minimum Gray | #767676 | 4.5:1 | AA |
| Blue | #0066CC | 4.5:1 | AA |
| Red | #B71C1C | 5.9:1 | AA |
| Green | #2E7D32 | 4.5:1 | AA |On Black Background (#000000)
| Text Color | Hex | Ratio | Passes |
|--------------|---------|--------|--------|
| White | #FFFFFF | 21:1 | AAA |
| Light Gray | #CCCCCC | 12.6:1 | AAA |
| Medium Gray | #A6A6A6 | 7.0:1 | AAA |
| Minimum Gray | #8F8F8F | 4.5:1 | AA |Taking Action
Color contrast issues are fixable. Use automated tools to identify problems, then systematically address each violation using the techniques in this guide. Build accessible color palettes into your design system to prevent future issues.
TestParty's automated monitoring catches contrast issues as they're introduced, preventing accessibility debt.
Schedule a TestParty demo and get a 14-day compliance implementation plan.
Related Resources
Stay informed
Accessibility insights delivered
straight to your inbox.


Automate the software work for accessibility compliance, end-to-end.
Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.
Book a Demo