← All Articles · · 12 min read

WCAG Color Contrast Guide: Accessible Web Design

Master WCAG color contrast requirements for accessible web design. Understand contrast ratios, AA vs AAA standards, how to test your designs, and practical tips for passing accessibility audits.

accessibilitywcagcolor-contrastweb-designcssa11yux

Color contrast is one of the most common accessibility failures on the web—and one of the most preventable. An estimated 1 in 12 men and 1 in 200 women have some form of color vision deficiency. For users with low vision or aging eyes, poor contrast doesn’t just make text hard to read; it makes it invisible.

WCAG (Web Content Accessibility Guidelines) defines specific contrast ratios that text and UI elements must meet. Getting this right is no longer optional—it’s a legal requirement in many jurisdictions and a core part of building software that actually works for everyone.

This guide explains the math behind contrast ratios, what WCAG requires, how to test your designs, and practical strategies for building accessible color systems. Use the DevPlaybook Color Picker to experiment with colors as you read.


What Is Color Contrast Ratio?

Contrast ratio is a mathematical expression of the luminance difference between two colors. It’s calculated using the relative luminance of each color as defined in the WCAG 2.x specification.

Relative Luminance Formula

Relative luminance represents a color’s perceived brightness on a scale from 0 (absolute black) to 1 (absolute white).

For each RGB channel (normalized to 0–1):

if channel <= 0.04045:
    C_linear = channel / 12.92
else:
    C_linear = ((channel + 0.055) / 1.055) ^ 2.4

Then luminance:

L = 0.2126 * R_linear + 0.7152 * G_linear + 0.0722 * B_linear

Contrast Ratio Formula

contrast ratio = (L1 + 0.05) / (L2 + 0.05)

Where L1 is the lighter color’s luminance and L2 is the darker color’s luminance. The result is always ≥ 1:1. White on white = 1:1. Black on white = 21:1 (maximum possible ratio).

In JavaScript:

function getLuminance(r, g, b) {
  const [rs, gs, bs] = [r, g, b].map(c => {
    const sRGB = c / 255;
    return sRGB <= 0.04045
      ? sRGB / 12.92
      : Math.pow((sRGB + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

function getContrastRatio(rgb1, rgb2) {
  const L1 = getLuminance(...rgb1);
  const L2 = getLuminance(...rgb2);
  const lighter = Math.max(L1, L2);
  const darker = Math.min(L1, L2);
  return (lighter + 0.05) / (darker + 0.05);
}

// Example
const ratio = getContrastRatio([0, 102, 204], [255, 255, 255]);
// #0066CC on white = ~4.56:1

WCAG Contrast Requirements

WCAG 2.1 and 2.2 define contrast requirements across two conformance levels: AA (the minimum for most legal and practical requirements) and AAA (enhanced accessibility).

Text Contrast (Success Criterion 1.4.3 — Level AA)

Text TypeMinimum Ratio (AA)Enhanced Ratio (AAA)
Normal text (< 18pt or < 14pt bold)4.5:17:1
Large text (≥ 18pt or ≥ 14pt bold)3:14.5:1
Text in logos or decorative textNo requirementNo requirement
Disabled textNo requirementNo requirement

In CSS terms:

  • Normal text: less than 24px regular weight, or less than 18.67px bold
  • Large text: 24px or larger at normal weight, or 18.67px or larger at bold

Non-Text Contrast (Success Criterion 1.4.11 — Level AA)

Since WCAG 2.1, UI components and graphical objects must also meet contrast requirements:

ElementMinimum Ratio
Form inputs (border vs. background)3:1
Focus indicators3:1
Icons (when essential to understanding)3:1
Chart elements (lines, bars, segments)3:1
State indicators (checked checkbox, active tab)3:1

Non-text contrast at 3:1 is lower than text because these elements convey structure or state rather than linguistic meaning.

WCAG 2.2 Updates

WCAG 2.2 (2023) added new requirements including:

  • Focus Visible (2.4.11 AA): Focus indicators must have a minimum area and contrast ratio. Specifically, the focus indicator must be at least as large as a 2px perimeter and have a 3:1 contrast ratio against adjacent colors.
  • Focus Not Obscured (2.4.12 AA): Focused components must not be entirely hidden by sticky headers or overlays.

These requirements affect how you style :focus-visible in CSS.


Common Failures (with Fixes)

Failure 1: Gray Text on White Background

/* Fails: #767676 on white = 4.48:1 (just below AA for normal text) */
.body-text {
  color: #767676;
  background: #ffffff;
}

/* Pass: #696969 on white = 5.07:1 */
.body-text {
  color: #696969;
  background: #ffffff;
}

/* Or use the slightly darker WCAG-compliant minimum */
.body-text {
  color: #757575; /* 4.6:1 on white — passes AA */
  background: #ffffff;
}

The exact minimum gray that passes WCAG AA on white is #767676 — but only barely (4.48:1 rounds to a fail in strict checkers). Use #757575 or darker for safety.

Failure 2: Blue Text on Blue Background

/* Fails: #4A90E2 on #2C5282 = 1.9:1 */
.badge {
  color: #4A90E2;
  background: #2C5282;
}

/* Pass: white on #2C5282 = 7.5:1 */
.badge {
  color: #ffffff;
  background: #2C5282;
}

Avoid using similar hue/saturation combinations. When in doubt, use white or black text.

Failure 3: Placeholder Text

/* Fails: #9CA3AF placeholder on white = 2.9:1 */
input::placeholder {
  color: #9CA3AF;
}

/* Pass: use slightly darker placeholder */
input::placeholder {
  color: #6B7280; /* 5.74:1 on white */
}

Placeholder text is technically not “required” content, but WCAG 2.1 clarified that if placeholder text conveys essential information, it must meet contrast requirements. Best practice: darken your placeholders.

Failure 4: Low-Contrast Focus Rings

/* Fails: thin blue border can be invisible on some backgrounds */
:focus {
  outline: 1px solid #4A90E2;
}

/* Pass: WCAG 2.2 compliant focus style */
:focus-visible {
  outline: 2px solid #0066CC;
  outline-offset: 2px;
  /* Verify #0066CC has 3:1 contrast against adjacent background */
}

Failure 5: White Text on Light Background in Dark Mode

/* Works in light mode but fails in dark mode if misconfigured */
.card {
  background: #1a1a2e;
  color: #ffffff; /* 15.5:1 — fine */
}

/* Subtle dark mode failure */
.muted {
  background: #2d2d2d;
  color: #9d9d9d; /* 3.0:1 — fails AA for normal text */
}

Dark mode is not automatically accessible. Recalculate contrast ratios for every color combination in your dark theme.


Building an Accessible Color System

Rather than checking colors individually, build a system where contrast is predictable.

The “10 Shades” Approach

Design your palette as a 10-step scale from 50 (lightest) to 900 (darkest). Establish rules:

Text ColorBackground Constraint
Shade 700+Always safe on white or shade 50–100
Shade 600Check against specific backgrounds
Shade 500Use for large text or UI elements only
Shade 100–Usually safe on shade 800+
/* Example: blue palette where 700+ passes on white */
:root {
  --blue-50: #eff6ff;
  --blue-100: #dbeafe;
  --blue-200: #bfdbfe;
  --blue-300: #93c5fd;   /* 2.6:1 on white — decorative only */
  --blue-400: #60a5fa;   /* 3.0:1 on white — large text only */
  --blue-500: #3b82f6;   /* 4.0:1 on white — close to AA */
  --blue-600: #2563eb;   /* 5.9:1 on white — passes AA */
  --blue-700: #1d4ed8;   /* 7.8:1 on white — passes AAA */
  --blue-800: #1e40af;   /* 10.1:1 on white */
  --blue-900: #1e3a8a;   /* 13.0:1 on white */
}

In this system, --blue-600 and above are safe for normal text on white.

Semantic Color Tokens

Rather than exposing raw hex values, use semantic tokens that encode accessibility intent:

:root {
  /* Text colors — all verified against white background */
  --text-primary: #111827;     /* 18.1:1 */
  --text-secondary: #374151;   /* 11.0:1 */
  --text-muted: #6B7280;       /* 5.74:1 — AA only */
  --text-disabled: #9CA3AF;    /* 2.9:1 — use only for disabled state */

  /* Interactive colors */
  --link-color: #1D4ED8;       /* 7.8:1 on white */
  --link-hover: #1E3A8A;       /* 13.0:1 on white */

  /* Focus ring */
  --focus-ring: #0066CC;       /* 4.56:1 on white */
}

Testing Color Contrast

Automated Testing

In code (during build/test):

# axe-core (most comprehensive)
npm install axe-core

# In Jest/Vitest
import { axe } from 'jest-axe';
const results = await axe(container);
expect(results).toHaveNoViolations();

CI/CD integration:

# pa11y — command-line accessibility checker
npx pa11y https://yoursite.com --standard WCAG2AA

# pa11y in CI
npx pa11y-ci --config .pa11yci.json
// .pa11yci.json
{
  "defaults": {
    "standard": "WCAG2AA",
    "runners": ["axe"]
  },
  "urls": [
    "http://localhost:3000",
    "http://localhost:3000/about"
  ]
}

Browser DevTools

Chrome:

  1. Open DevTools → Elements → Computed
  2. Click a text element
  3. Under “Color”, DevTools shows contrast ratio with a pass/fail indicator

Firefox:

  1. Open DevTools → Accessibility
  2. Enable “Show Tabbing Order” to visualize focus flow
  3. The Accessibility Inspector shows contrast ratios per element

Design Tools

Figma:

  • Plugin: Contrast by Figma (free)
  • Plugin: A11y - Color Contrast Checker
  • Use the built-in Color Blindness Simulation to preview your design across 8 types of color vision

Storybook:

npm install @storybook/addon-a11y

Add to .storybook/main.js:

export default {
  addons: ['@storybook/addon-a11y']
};

This runs axe-core on each story and shows violations in the “Accessibility” panel.


Color Vision Deficiency: What Users Actually See

Understanding the types of color blindness helps you design better, not just pass a ratio check.

TypePrevalenceWhat’s affected
Deuteranopia (red-green)~5% of menGreen deficiency—reds/greens look similar
Protanopia (red-green)~1% of menRed deficiency—reds appear dark
Tritanopia (blue-yellow)<0.01%Blue-yellow confusion
AchromatopsiaVery rareNo color vision—sees only luminance

Key insight: Contrast ratio is based on luminance, not hue. This is why a red (#CC0000) and green (#008800) can have poor contrast even though they “look different” to most people—their luminance values are similar.

// Red vs green — perceived as very different, but luminance is close
getContrastRatio([204, 0, 0], [0, 136, 0])  // ≈ 1.3:1 — terrible contrast

Never rely on color alone to convey information. Always pair color with:

  • Text labels
  • Icons
  • Patterns or shapes
  • Position

WCAG 3.0 Preview: APCA

The upcoming WCAG 3.0 uses a new algorithm called APCA (Advanced Perceptual Contrast Algorithm). APCA:

  • Accounts for text size and weight in the calculation (not just as thresholds)
  • Uses a perceptual lightness model more aligned with human vision (Lc scale)
  • Handles dark backgrounds differently from light backgrounds (contrast is not symmetric)
  • Produces scores from Lc 0 to Lc 106 rather than ratios

APCA is currently in beta as part of the WCAG 3 working draft. If you want to future-proof:

  • Aim for Lc 60+ for body text (stricter than WCAG 2.x in some cases)
  • Use the APCA Contrast Calculator to preview scores
  • The current WCAG 2.x requirements remain legally binding—APCA is additive, not a replacement yet

Quick Reference: Safe Text Colors on Common Backgrounds

BackgroundMinimum Safe Dark TextMinimum Safe Light Text
White (#fff)#767676 (barely—use #757575)n/a
Light gray (#f3f4f6)#6B7280n/a
Medium blue (#2563EB)n/a#ffffff
Dark gray (#1F2937)n/a#9CA3AF (large text only)
Dark navy (#1E3A8A)n/a#ffffff

Summary

Accessible color contrast comes down to three practical habits:

  1. Build a verified color scale — establish which shades are safe for text vs. decoration before writing any CSS
  2. Test in context — automated tools catch most issues; supplement with manual DevTools checks for interactive states (hover, focus, disabled)
  3. Never rely on color alone — pair color cues with text, icons, or other non-color signals

The DevPlaybook Color Picker lets you explore colors and verify RGB/hex values as you design. For full contrast ratio calculations, paste your hex values into any of the checker tools above.

The 4.5:1 ratio for normal text and 3:1 for large text and UI components are the numbers to know. Everything else in this guide is context for meeting them correctly.


Working with accessibility in CSS? The DevPlaybook CSS Flexbox Guide shows how to build layout patterns that work correctly with screen readers. For consistent design tokens, explore the Color Picker to build your accessible palette.

Free Newsletter

Level Up Your Dev Workflow

Get new tools, guides, and productivity tips delivered to your inbox.

Plus: grab the free Developer Productivity Checklist when you subscribe.

Found this guide useful? Check out our free developer tools.