Guides / Color
accessibility

Building an Accessible Color Palette for WCAG

Accessibility isn't a constraint on good design — it's a constraint that makes design better. A color palette built around contrast requirements from day one is more systematic, more professional, and reaches 15% more users who have visual impairments.

April 2026 · 9 min read

WCAG Contrast Requirements at a Glance

StandardRatioApplies ToWho It Covers
WCAG AA (minimum)4.5:1Normal text (< 18pt / 14pt bold)Legal requirement in many countries
WCAG AA (minimum)3:1Large text (≥ 18pt / 14pt bold)Legal requirement in many countries
WCAG AA (minimum)3:1UI components (buttons, inputs, icons)Legal requirement in many countries
WCAG AAA (enhanced)7:1Normal textRecommended for critical content
WCAG AAA (enhanced)4.5:1Large textRecommended 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.