Skip to content
Back to Quality Assurance

Color Contrast

Ensure text and interactive elements have sufficient contrast against the background for low-vision users.

Why

Color contrast is the difference in luminance between the text and the background, and it determines whether the text is readable for the 2.2 billion people with low vision. WCAG 1.4.3 requires a contrast ratio of 4.5:1 for normal text and 3:1 for large text (Level AA, the legal minimum). A light-gray-on-white button that looks elegant to a designer is invisible to a user with cataracts, and a 2:1 ratio that is "fine on my monitor" is unreadable in bright sunlight on a phone. Contrast is the easiest a11y issue to check (it is a number) and the easiest to fix (darken the text or lighten the background), yet it is the most common violation because designers optimize for aesthetics on a calibrated monitor, not for the real-world conditions where the user reads the text.

When

Every text element, every interactive element's border/fill, and every meaningful icon. Check in design, in CI (automated), and before launch.

How

1. Check every text/background pair with a contrast tool
   - Browser DevTools: inspect an element → Color → click the contrast ratio
   - axe-core: automated check in CI (catches most violations)
   - WebAIM Contrast Checker: paste hex values for a quick check
   - Stark or Contrast (Figma/Sketch plugins): check in the design tool

2. Understand the WCAG thresholds
   - Normal text (< 18pt or < 14pt bold): 4.5:1 minimum (AA), 7:1 (AAA)
   - Large text (>= 18pt or >= 14pt bold): 3:1 minimum (AA), 4.5:1 (AAA)
   - UI components (buttons, inputs, icons): 3:1 against the background
   - Disabled elements: exempt (but ensure they look disabled, not just low-contrast)

3. Fix low-contrast pairs
   - Darken the text color (most common fix — #666 → #333)
   - Lighten the background (white instead of light gray)
   - Increase the text size (moves from 4.5:1 to 3:1 threshold)
   - Add a border or shadow to separate the element from the background

4. Check the non-text elements
   - Icon buttons: the icon must have 3:1 contrast against its background
   - Form field borders: the border must have 3:1 contrast
   - Charts and data viz: each data series must be distinguishable (not just by color)
   - Links: distinguishable from non-link text (not just by color — underline or weight)

5. Test in real-world conditions
   - View the page in bright sunlight (or increase screen brightness to simulate)
   - View the page with a high-contrast mode (Windows: Alt+Shift+PrintScreen)
   - View the page with a color blindness simulator (Stark, Coblis) — do not rely on color alone

Example

// Check contrast in DevTools
// 1. Inspect the element
// 2. In the Styles panel, click the color swatch
// 3. DevTools shows the contrast ratio and a pass/fail for AA and AAA

// CSS: fix low-contrast text
// Before: #999 on #fff = 2.85:1 (fails AA)
.muted-text { color: #999; }

// After: #666 on #fff = 5.74:1 (passes AA)
.muted-text { color: #666; }

// Or: #767676 on #fff = 4.54:1 (just passes AA — the GitHub gray)
.muted-text { color: #767676; }

// Links: do not rely on color alone
// Bad: links are only a different color, indistinguishable to color-blind users
a { color: #0066cc; } /* blue, no underline */

// Good: links have an underline (or other non-color indicator)
a { color: #0066cc; text-decoration: underline; }

// Charts: do not rely on color alone to distinguish series
// Bad: red and green lines (indistinguishable to red-green color blindness)
<line stroke="red" />
<line stroke="green" />

// Good: different colors + different patterns (solid, dashed)
<line stroke="red" stroke-dasharray="0" />
<line stroke="green" stroke-dasharray="4" />
// Or add labels directly to each line

// CI: axe-core checks contrast automatically
test("no contrast violations", async ({ page }) => {
  const results = await new AxeBuilder({ page })
    .withTags(["wcag2aa"])
    .analyze();
  const contrastIssues = results.violations.filter(
    v => v.id === "color-contrast"
  );
  expect(contrastIssues).toEqual([]);
});

Common Problems

  • ⚠Using the designer's chosen gray (#999) for muted text because it "looks right" — it has a 2.85:1 ratio and fails WCAG AA, but the team does not check because it looks fine on a calibrated monitor.
  • ⚠Distinguishing links from body text by color alone — the 8% of men with red-green color blindness cannot tell the blue link from the black text, and the link is invisible to them.
  • ⚠Using red and green to indicate error and success — the most common form of color-only encoding, and the most common accessibility barrier for color-blind users (who see both as the same brown).
  • ⚠Checking contrast on a design tool's default white background, but the app has a dark mode or a colored background where the same text color fails — the contrast must be checked for every theme.

Best Practices

  • ✓Run axe-core in CI to catch contrast violations automatically — it checks every text/background pair and fails the build on a violation, preventing the most common a11y issue from shipping.
  • ✓Use a contrast ratio of 4.5:1 for all text (not just the WCAG minimum) — the extra margin accounts for screen glare, aging displays, and the real-world conditions where the user reads the text.
  • ✓Never rely on color alone to convey meaning — pair color with a text label, an icon, a pattern, or an underline, so the meaning is preserved for users who cannot distinguish the color.
  • ✓Check contrast in every theme (light, dark, high-contrast) — a text color that passes in light mode can fail in dark mode, and the check must be run for each.