Blog

How to Fix Color Contrast Issues: WCAG 2.2 Remediation Tutorial

TestParty
TestParty
April 29, 2025

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 displayed

Command line testing:

# Using pa11y
npx pa11y https://example.com --include-warnings

# Output includes contrast violations:
# • Error: WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail

Online 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 luminance

Using Tools

Browser DevTools (Chrome):

  1. Inspect element
  2. Click the color swatch in Styles panel
  3. View "Contrast ratio" in color picker
  4. See if it passes AA/AAA

WebAIM Contrast Checker:

  1. Enter foreground color (hex, RGB, or HSL)
  2. Enter background color
  3. 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 */
}

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 resolved

Cross-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.


Stay informed

Accessibility insights delivered
straight to your inbox.

Contact Us

Automate the software work for accessibility compliance, end-to-end.

Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.

Book a Demo