Skip to main content
Accessibility

How to Fix Low Color Contrast Ratio (WCAG Compliance)

What contrast ratio means, why WCAG requires minimum ratios, how to check your colors, and how to fix low contrast without redesigning.

Updated

How to Fix Low Color Contrast Ratio (WCAG Compliance)

The warning in your accessibility audit:

Elements must have sufficient color contrast ratio (4.5:1 for normal text, 3:1 for large text)

This means the foreground text color and the background color behind it are too similar. People with low vision, color blindness, or even just a bright screen glare cannot read the text. WCAG (Web Content Accessibility Guidelines) defines minimum contrast ratios to prevent this.

What contrast ratio means

Contrast ratio is a number between 1:1 (no contrast, same color) and 21:1 (maximum contrast, black on white). It compares the relative luminance of two colors. The formula accounts for how the human eye perceives brightness, which is why pure blue (#0000FF) on black has a lower perceived contrast than pure green (#00FF00) on black, even though both are “bright” colors.

You don’t need to calculate the ratio manually. Tools do it for you. What matters is knowing the thresholds.

WCAG thresholds

AA level (the standard most sites target)

  • Normal text (under 18pt, or under 14pt bold): minimum 4.5:1
  • Large text (18pt+, or 14pt+ bold): minimum 3:1
  • UI components and graphical objects (icons, form borders, focus indicators): minimum 3:1

AAA level (stricter, harder to achieve)

  • Normal text: minimum 7:1
  • Large text: minimum 4.5:1

Most teams aim for AA. AAA is recommended for body text on content-heavy sites, but it’s not a legal requirement in most jurisdictions.

How to check your contrast

1. Chrome DevTools

Inspect any text element, click the color swatch in the Styles panel, and Chrome shows the contrast ratio with a pass/fail indicator for AA and AAA. It also draws two lines on the color picker showing the boundary where the ratio drops below each threshold.

2. Lighthouse audit

Run Lighthouse (DevTools > Lighthouse tab) with the Accessibility category checked. It flags every text element that fails the contrast ratio check and tells you the current ratio.

3. Browser extensions

axe DevTools and WAVE are the most reliable. They scan the full page and produce a list of every failing element with its current ratio and the minimum required.

4. Design tools

Figma plugins like Stark and A11y check contrast during the design phase. Fixing contrast before code is cheaper than fixing it after.

Common patterns that fail (and how to fix them)

1. Gray text on white backgrounds

The most widespread failure. Light gray placeholder text, muted labels, and secondary text often fall below 4.5:1.

/* Fails: #999 on #fff = 2.85:1 */
.muted-text {
  color: #999999;
  background: #ffffff;
}

/* Passes AA: #767676 on #fff = 4.54:1 */
.muted-text {
  color: #767676;
  background: #ffffff;
}

The magic number for gray on white is #767676. That’s the lightest gray that passes 4.5:1 against pure white. If you need lighter text, the background must get darker too.

2. Light text on colored backgrounds

Buttons, banners, and badges with white or light text on a brand color. Many brand colors are too light for white text.

/* Fails: #fff on #5bc0de = 2.94:1 */
.info-badge {
  color: #ffffff;
  background: #5bc0de;
}

/* Passes: #fff on #2779bd = 4.56:1 */
.info-badge {
  color: #ffffff;
  background: #2779bd;
}

Fix: Darken the background color until the ratio passes. Use a contrast checker in real time while adjusting the hex value.

3. Text on images or gradients

Text overlaying a photograph or gradient has no single background color. The contrast depends on what part of the image sits behind the text, and it changes with different images.

/* Add a semi-transparent overlay to guarantee minimum contrast */
.hero-overlay {
  position: relative;
}

.hero-overlay::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 1;
}

.hero-overlay .text {
  position: relative;
  z-index: 2;
  color: #ffffff;
}

The overlay darkens the image uniformly so white text always has enough contrast. Adjust the opacity (0.55 in this example) until the ratio passes against the lightest part of the image.

4. Placeholder text in inputs

Browser default placeholder text is often too light. Don’t rely on the browser default.

/* Most browsers default to around #a9a9a9, which fails on white */
::placeholder {
  color: #767676;
}

5. Focus indicators that disappear

Custom focus styles that use a light outline on a light background fail the 3:1 requirement for UI components.

/* Fails on white background */
:focus-visible {
  outline: 2px solid #b3d9ff;
}

/* Passes: 3:1+ against white */
:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

Using CSS custom properties for systematic fixes

If your site uses design tokens or CSS custom properties, fix contrast at the token level instead of on individual elements.

:root {
  --color-text-primary: #1a1a1a;    /* 16.75:1 on white */
  --color-text-secondary: #525252;  /* 7.14:1 on white */
  --color-text-muted: #767676;      /* 4.54:1 on white */
  --color-bg-primary: #ffffff;
}

/* Dark mode tokens with matching contrast */
@media (prefers-color-scheme: dark) {
  :root {
    --color-text-primary: #f5f5f5;    /* 15.3:1 on #1a1a1a */
    --color-text-secondary: #a3a3a3;  /* 7.24:1 on #1a1a1a */
    --color-text-muted: #8a8a8a;      /* 4.64:1 on #1a1a1a */
    --color-bg-primary: #1a1a1a;
  }
}

When you update a token, every component using it gets the fix. This is how you avoid playing whack-a-mole with individual elements.

Prevention

Add contrast checks to your CI pipeline. Tools like axe-core can run as part of your test suite:

import { axe } from 'vitest-axe'
import { render } from '@testing-library/react'

test('page has no contrast violations', async () => {
  const { container } = render(<MyPage />)
  const results = await axe(container)
  expect(results.violations.filter(v => v.id === 'color-contrast')).toHaveLength(0)
})

This catches regressions before they ship. Combine it with the design token approach, and contrast issues become rare.

Hushbug flags accessibility issues including contrast ratio failures automatically while you browse. Coming soon to the Chrome Web Store.