WCAG Contrast Requirements at a Glance
| Standard | Ratio | Applies To | Who It Covers |
|---|---|---|---|
| WCAG AA (minimum) | 4.5:1 | Normal text (< 18pt / 14pt bold) | Legal requirement in many countries |
| WCAG AA (minimum) | 3:1 | Large text (≥ 18pt / 14pt bold) | Legal requirement in many countries |
| WCAG AA (minimum) | 3:1 | UI components (buttons, inputs, icons) | Legal requirement in many countries |
| WCAG AAA (enhanced) | 7:1 | Normal text | Recommended for critical content |
| WCAG AAA (enhanced) | 4.5:1 | Large text | Recommended for critical content |
The Accessible Palette Strategy
Design your palette with contrast in mind from the start. Create a scale from lightest to darkest, then identify which pairs meet which requirements.
/* Accessible blue palette example */
:root {
--blue-50: hsl(214, 100%, 97%); /* #EBF5FF */
--blue-100: hsl(214, 95%, 92%); /* #D6EDFF */
--blue-200: hsl(214, 90%, 83%); /* #A8D4FF */
--blue-300: hsl(214, 85%, 70%); /* #6DB6FF */
--blue-400: hsl(214, 80%, 56%); /* #3494EE */
--blue-500: hsl(214, 75%, 44%); /* #1A6FBF → 4.8:1 on white (AA ✓) */
--blue-600: hsl(214, 72%, 35%); /* #155A9C → 7.3:1 on white (AAA ✓) */
--blue-700: hsl(214, 68%, 26%); /* #10437A → 10.5:1 on white */
--blue-800: hsl(214, 64%, 18%); /* #0B2E54 */
--blue-900: hsl(214, 60%, 10%); /* #061C30 */
}
/*
Text on white bg:
--blue-500 → 4.8:1 → AA ✓ for body text
--blue-600 → 7.3:1 → AAA ✓ for all text
Interactive elements (3:1 required):
--blue-400 → 3.1:1 → AA ✓ for buttons/links on white
Dark mode (text on dark bg):
--blue-300 on --blue-900 → 9.4:1 → AAA ✓
--blue-200 on --blue-900 → 12.1:1 → AAA ✓
*/
Checking Contrast Programmatically
/* JavaScript: calculate WCAG contrast ratio */
function getLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(hex1, hex2) {
const toRGB = hex => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b];
};
const l1 = getLuminance(...toRGB(hex1));
const l2 = getLuminance(...toRGB(hex2));
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Usage
const ratio = getContrastRatio('#1A6FBF', '#FFFFFF');
console.log(ratio.toFixed(2)); // → 4.82 (AA pass)
const passes = {
AA_normal: ratio >= 4.5,
AA_large: ratio >= 3,
AAA_normal: ratio >= 7,
AAA_large: ratio >= 4.5,
};
Accessible Brand Colors: Strategy
Most brand colors fail WCAG when used as text on white. The solution isn't to abandon your brand color — it's to darken it for text use and use the vibrant version only for large elements.
/* Brand color: #7C6FFF (vibrant purple) */
/* Contrast on white: 3.2:1 → FAILS AA for normal text */
/* Solutions: */
/* 1. Darken for text use */
--brand-text: hsl(252, 80%, 42%); /* 5.1:1 on white → AA ✓ */
/* 2. Use only for large text or UI components (3:1 threshold) */
.button-primary {
background: #7C6FFF; /* 3.2:1 on white is OK for UI component */
color: white; /* white on #7C6FFF = 4.9:1 → AA ✓ for button text */
}
/* 3. Use on dark backgrounds (where the vibrant color passes) */
.badge-on-dark {
background: #7C6FFF;
/* #7C6FFF on #050508 = 8.1:1 → AAA ✓ */
}
/* 4. Tint for backgrounds (use very light tint, dark text on it) */
.alert {
background: hsl(252, 100%, 97%); /* very light brand tint */
color: hsl(252, 80%, 28%); /* dark brand text → 7.8:1 */
}
Common Accessible Color Mistakes
- Using brand color directly for body text — vibrant brand colors typically have 2–4:1 contrast on white. Darken them by 10–20% lightness for text use.
- Only checking text, not UI components — WCAG 1.4.11 requires 3:1 for borders, icons, and form elements. A light gray checkbox border on white often fails.
- Assuming dark mode is automatically accessible — light text on dark background needs the same contrast ratios. Recheck all your color pairs in dark mode.
- Using color as the only differentiator — for colorblind users, never convey information by color alone. Pair color with icons, labels, or patterns.
- Not testing with real tools — eyeballing contrast doesn't work. Use axe, Lighthouse, or StudioLimb's contrast checker to verify every text/background combination.
Frequently Asked Questions
- Does my website legally have to be WCAG AA compliant?
- In many countries, yes — especially for government, education, and public-facing commercial sites. The US (Section 508, ADA), EU (EN 301 549), and UK (PSBAR) all reference WCAG 2.1 AA. Even where not legally required, accessibility reduces liability risk and expands your audience.
- What's the difference between WCAG 2.1 and WCAG 3.0?
- WCAG 3.0 (in development) introduces APCA (Advanced Perceptual Contrast Algorithm), which better models human vision than the current relative luminance formula. For now, design to WCAG 2.1 AA — it's the enforceable standard. WCAG 3.0 won't replace 2.1 for regulatory purposes for several years.
- Does placeholder text need to meet contrast requirements?
- No — placeholder text is specifically exempt from WCAG 1.4.3 contrast requirements. However, it's still good practice to make placeholder text reasonably readable (aim for 3:1 minimum even if not required), as very light placeholder text frustrates users without disabilities too.
- How do I make focus indicators accessible?
- WCAG 2.2 added Success Criterion 2.4.11 (Focus Appearance): the focus indicator must have at least 3:1 contrast with adjacent colors and a minimum area of the component's perimeter × 2px. The simplest compliant focus style: a 2px solid outline in your darkened brand color with 2px offset.