Color contrast accessibility is one of the most commonly failed WCAG criteria — and one of the easiest to fix once you understand the rules. Here's what the standard requires and how to verify compliance.
What WCAG says
The Web Content Accessibility Guidelines (WCAG) define minimum contrast ratios between text and its background. The key requirements:
WCAG 2.1 Level AA (legal minimum in many jurisdictions):
- Normal text (under 18pt regular or 14pt bold): 4.5:1
- Large text (18pt+ regular or 14pt+ bold): 3:1
- UI components and graphical objects: 3:1
- Decorative elements, logos, disabled controls: exempt
WCAG 2.1 Level AAA:
- Normal text: 7:1
- Large text: 4.5:1
Legal context: WCAG 2.1 AA is legally required for public-sector websites in the EU (EN 301 549), the US (ADA and Section 508), Canada (AODA), and Australia (DDA). Private-sector websites are increasingly covered by ADA case law.
How contrast ratio is calculated
The contrast ratio is calculated from the relative luminance of the two colors:
``
contrast ratio = (L1 + 0.05) / (L2 + 0.05)
`
Where L1 is the lighter color's relative luminance and L2 is the darker color's relative luminance. Relative luminance ranges from 0 (black) to 1 (white).
Relative luminance formula for sRGB:
`javascript
function relativeLuminance(r, g, b) {
// Linearize each channel
const linear = [r, g, b].map(channel => {
const c = channel / 255;
return c <= 0.03928
? c / 12.92
: Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 linear[0] + 0.7152 linear[1] + 0.0722 * linear[2];
}
function contrastRatio(rgb1, rgb2) {
const l1 = relativeLuminance(...rgb1);
const l2 = relativeLuminance(...rgb2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
contrastRatio([47, 133, 90], [255, 255, 255])
// → 5.08 (passes AA for normal text)
`
For quick checking without code, a color contrast checker calculates the ratio and WCAG verdict instantly when you enter the two hex values.
Common contrast failures
Gray text on white:
Many design systems use light gray text for secondary/placeholder content. Typical failures:
| Text color | Background | Ratio | Result |
|-----------|-----------|-------|--------|
| #999999 | #ffffff | 2.85:1 | Fails AA |
| #767676 | #ffffff | 4.54:1 | Passes AA |
| #595959 | #ffffff | 7.22:1 | Passes AAA |
The lightest gray that passes AA on white is approximately #767676.
White text on color:
| Background | Ratio | Result |
|-----------|-------|--------|
| #3182ce (medium blue) | 4.6:1 | Passes AA |
| #e53e3e (medium red) | 4.07:1 | Fails AA for normal text |
| #d69e2e (medium yellow) | 2.21:1 | Fails AA |
Yellow and light colors almost never achieve sufficient contrast with white text. Use dark text on yellow.
Dark mode issues:
Inverting a light-mode color scheme doesn't guarantee contrast compliance. Check both modes separately.
Automating contrast checks
Storybook a11y addon:
<code>bash
<p>npm install --save-dev @storybook/addon-a11y</p>
</code>
Add to .storybook/main.js:
<code>javascript
<p>module.exports = { addons: ['@storybook/addon-a11y'] };</p>
</code>
The a11y addon runs axe accessibility checks on each story, including contrast ratio.
Playwright / Cypress:
`javascript
// Using axe-core with Playwright
import { checkA11y } from 'axe-playwright';
test('passes contrast checks', async ({ page }) => {
await page.goto('/');
await checkA11y(page, null, {
runOnly: { type: 'tag', values: ['wcag2aa'] }
});
});
`
Lighthouse:
Run from the command line:
<code>bash
<p>npx lighthouse https://example.com --only-categories=accessibility --output json</p>
</code>
Lighthouse's accessibility score covers contrast ratios for text elements automatically.
axe-core in browser:
<code>javascript
<p>// Install: npm install axe-core</p>
<p>import axe from 'axe-core';</p>
<p>axe.run(document, { runOnly: { type: 'tag', values: ['wcag2aa'] } })</p>
<p>.then(results => {</p>
<p>results.violations.forEach(v => {</p>
<p>console.log(v.id, v.description, v.nodes);</p>
<p>});</p>
<p>});</p>
</code>
Design tool support
Figma: Select a text layer, check the Fill section in the right panel — Figma shows the contrast ratio and AA/AAA pass/fail against the element behind it. The "Figma A11y" plugin and "Contrast" plugin provide more detailed analysis.
Sketch: The "Stark" plugin provides WCAG contrast checking inline.
Adobe XD: Built-in accessibility checker includes contrast. The "Able" plugin provides additional detail.
Fixing low-contrast designs
When a design fails contrast:
Option 1: Darken the text color (preserves the design's background)
`
css
/ Failed: #999 on white /
color: #999999;
/ Fixed: darkened to pass AA /
color: #767676;
`
Option 2: Lighten the background
For text on a dark background that's too dark — lighten the background until contrast passes.
Option 3: Use an intermediate shade for all states
If a brand color fails contrast as text-on-white, use a darker shade:
<code>css
<p>:root {</p>
<p>--brand: hsl(145, 48%, 45%); /<em> Too light for text on white </em>/</p>
<p>--brand-accessible: hsl(145, 48%, 35%); /<em> Darker, passes AA </em>/</p>
<p>}</p>
</code>`
Check WCAG 3.0:
WCAG 3.0 introduces the APCA (Advanced Perceptual Contrast Algorithm), which is more accurate for low-contrast situations and replaces the simple ratio with a perceptual model. It's not yet a legal standard, but design tools and validators are beginning to include it alongside the traditional ratio.
Color contrast is a straightforward accessibility requirement with clear pass/fail criteria. Building contrast checking into your design review and CI pipeline catches failures before they reach production.
Originally published at https://snappytools.app/color-contrast-checker/