Skip to main content
{/}codefunc
CSS & DesignJune 25, 2026 · 5 min read

A11y Color Contrast: WCAG AA vs AAA and How to Fix Failing UI

A practical a11y guide to color contrast ratios, WCAG 2.2 success criteria, and a step-by-step workflow for auditing text, icons, and interactive states.

By codefunc

accessibilitywcagcontrasta11ydesign-system

Low contrast is the most common accessibility failure on production sites — and the easiest to prevent if you check ratios before ship. Gray #767676 text on white fails for small body copy. Brand primary buttons with light green on white fail for millions of color-blind users. This guide explains WCAG contrast requirements, how to measure them correctly, and how to fix failures without abandoning your palette.

Bottom line: Normal text needs 4.5:1 contrast against its background (WCAG AA). Large text needs 3:1. UI components and graphical objects need 3:1. Check contrast on every state — hover, focus, disabled — not just the default mockup.

What WCAG requires

Web Content Accessibility Guidelines (WCAG) 2.2 define contrast minimum success criteria:

Criterion Level Requirement
1.4.3 Contrast (Minimum) AA 4.5:1 normal text, 3:1 large text
1.4.6 Contrast (Enhanced) AAA 7:1 normal text, 4.5:1 large text
1.4.11 Non-text Contrast AA 3:1 for UI components and graphics

Most legal frameworks reference Level AA as the baseline (EU Accessibility Act, ADA settlements, Section 508).

What counts as "large text"

Style WCAG definition
Large ≥ 18pt (24px) regular or ≥ 14pt (18.66px) bold
Normal Everything else

A 16px regular paragraph is normal text — 4.5:1 applies.

How contrast ratio is calculated

Contrast ratio compares relative luminance of foreground and background:

(L1 + 0.05) / (L2 + 0.05)

Where L1 is the lighter color. Range is 1:1 (no contrast) to 21:1 (black on white).

You do not need to compute this manually — use a contrast checker. You do need to know which color pairs to test.

What to test (the full matrix)

Text

  • Body copy on page background
  • Muted/secondary text on background
  • Text on colored cards, badges, banners
  • Links in default, hover, visited, focus states
  • Placeholder text (should meet contrast or not rely on placeholder alone)
  • Error and success messages

Non-text (WCAG 1.4.11)

  • Button borders and fills vs adjacent background
  • Form input borders vs background
  • Icons conveying meaning (not purely decorative)
  • Focus rings vs background
  • Chart lines and data visualization elements

States teams forget

State Example failure
Disabled #ccc text on #f5f5f5 — looks "disabled" but unreadable
Hover Button lightens on hover below 4.5:1
Focus Thin #ddd focus ring invisible on white
Dark mode Passes in light, fails when theme switches

Test both themes if you ship dark mode.

Worked examples

Failing muted text

/* 3.2:1 — fails AA for body text */
.muted {
  color: #999999;
  background: #ffffff;
}

Fix options:

  1. Darken text: #595959 → ~7:1 on white
  2. Lighten background if context allows
  3. Increase font size/weight to qualify as "large" (last resort — do not abuse)

Passing brand button

.button {
  color: #ffffff;
  background: #1a56db; /* ~6.5:1 white on blue */
}

.button:hover {
  background: #1e429f; /* maintain or improve ratio */
}

Focus visible

:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

Outline must contrast with adjacent colors, not only the element fill.

AA vs AAA: which target?

Target When to use
AA Legal baseline, most products, public websites
AAA Health, finance, government, accessibility-first products

AAA for all body text is hard with soft brand palettes. Common compromise: AA for body, AAA for headings and primary actions where possible.

Color blindness and contrast

Contrast ratio is not the same as color distinguishability. Two colors can pass 4.5:1 but be indistinguishable for deuteranopia users (red/green confusion).

Pair contrast with:

  • Icons + text labels (not color alone for errors)
  • Patterns or underlines on charts
  • prefers-color-scheme and forced-colors media queries

Design system workflow

1. Define semantic tokens

:root {
  --text-primary: #111827;
  --text-secondary: #4b5563;   /* verify 4.5:1 on bg */
  --text-muted: #6b7280;       /* borderline — check size */
  --bg-surface: #ffffff;
  --interactive-primary: #1d4ed8;
}

2. Document tested pairs in your design system

Token pair Ratio WCAG Notes
text-primary / bg-surface 16.6:1 AAA Body
text-secondary / bg-surface 7.0:1 AAA Captions OK
text-muted / bg-surface 4.6:1 AA Min for 16px

3. Gate PRs with automated checks

  • ESLint plugin eslint-plugin-jsx-a11y for obvious issues
  • Storybook a11y addon on component stories
  • CI screenshot tests with contrast regression (optional)

4. Fix failures systematically

  1. Identify failing pair
  2. Adjust foreground first (preserve brand background)
  3. Re-check all states
  4. Update Figma tokens to match code

Gradients and images behind text

Contrast applies to the immediate background pixels behind text. Text on a busy photo fails unless you add an overlay:

.hero {
  background-image: url("/hero.jpg");
}

.hero-title {
  color: #ffffff;
  background: rgba(0, 0, 0, 0.55);
  padding: 0.25em 0.5em;
  box-decoration-break: clone;
}

Test the worst-case region of the image (lightest patch behind white text).

Common excuses (and rebuttals)

Excuse Reality
"Brand won't allow darker gray" Accessible brand ≠ original hex — adjust hue, keep identity
"It's just placeholder text" Users still read placeholders
"Disabled doesn't need contrast" WCAG still requires readable disabled text or alternative cues
"We'll fix in v2" Lawsuits and lost users happen in v1

Use these codefunc tools alongside this guide:

Practical takeaway

Aim for WCAG AA minimum on all shipped UI. Test text and non-text elements. Test hover, focus, and dark mode. Document token pairs in your design system. Fix contrast at the token level — not one-off hotfixes per component.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool