Contrast and Accessibility

Accessible color choices keep your designs usable for everyone, including the 300+ million people with color vision deficiencies. Not optional. Essential.

Why Accessibility Matters

  • 8% of men and 0.5% of women have color vision deficiency
  • Situational impairments affect everyone (bright sunlight, tired eyes, old screens)
  • Legal requirements. Many countries mandate accessible digital products.
  • Better for everyone. High-contrast designs are easier for all users.

Understanding Contrast Ratio

Contrast ratio measures the difference in luminance between two colors.

The Scale

1:1    ████████████  Same color (no contrast)
3:1    ████████████  Minimum for large text
4.5:1  ████████████  Minimum for normal text (AA)
7:1    ████████████  Enhanced contrast (AAA)
21:1   ████████████  Black on white (maximum)

How It's Calculated

Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)

Where L1 = luminance of the lighter color
      L2 = luminance of the darker color
      Luminance ranges from 0 (black) to 1 (white)

You do not need to calculate this by hand. Use a tool (listed below).

WCAG Requirements

The Web Content Accessibility Guidelines (WCAG) define minimum contrast standards.

Level AA (Minimum - Aim for This)

ElementMinimum RatioExample
Normal text (<18px or <14px bold)4.5:1Body copy, labels
Large text (≥18px or ≥14px bold)3:1Headings
UI components & graphics3:1Buttons, icons, form fields

Level AAA (Enhanced)

ElementMinimum Ratio
Normal text7:1
Large text4.5:1

Target AA for everything, AAA for critical content.

Practical Contrast Guidelines

Text on Backgrounds

/* PASS: 4.5:1 or higher for normal text */
.good-contrast {
  color: hsl(220, 10%, 20%);       /* Dark gray text */
  background: hsl(0, 0%, 100%);    /* White background */
  /* Ratio: ~12:1, PASS */
}

/* FAIL: below 4.5:1 */
.bad-contrast {
  color: hsl(220, 10%, 60%);       /* Light gray text */
  background: hsl(0, 0%, 100%);    /* White background */
  /* Ratio: ~3:1, FAIL */
}

Text Color Recommendations

For light backgrounds (white or light gray):

:root {
  /* Body text: needs 4.5:1 minimum */
  --text-primary: hsl(220, 15%, 20%);   /* ~13:1 on white, PASS */

  /* Secondary text: still needs 4.5:1 */
  --text-secondary: hsl(220, 10%, 40%); /* ~6:1 on white, PASS */

  /* Muted/tertiary: large text only (3:1) or decorative */
  --text-muted: hsl(220, 10%, 55%);     /* ~4:1 on white, borderline */
}

For dark backgrounds:

:root {
  --dark-bg: hsl(220, 20%, 12%);

  /* Light text on dark: same rules apply */
  --text-on-dark: hsl(220, 15%, 90%);            /* ~11:1, PASS */
  --text-secondary-on-dark: hsl(220, 10%, 70%);  /* ~6:1, PASS */
}

Interactive Elements

Buttons, links, and form controls need 3:1 against adjacent colors:

/* Button needs 3:1 against background */
.button {
  background: hsl(220, 80%, 50%);  /* Blue button */
  /* Against white page: ~4.5:1, PASS */
}

/* Focus indicator needs 3:1 against both the element AND the background */
.button:focus {
  outline: 3px solid hsl(220, 80%, 40%);
  outline-offset: 2px;
}

Form Fields

/* Border needs 3:1 against background */
.input {
  border: 1px solid hsl(220, 10%, 70%);  /* ~2.5:1 on white, FAIL */
}

/* Better */
.input {
  border: 1px solid hsl(220, 10%, 55%);  /* ~4:1 on white, PASS */
}

/* Or thicker borders */
.input {
  border: 2px solid hsl(220, 10%, 65%);  /* Thickness helps visibility */
}

Color Blindness Considerations

Types of Color Vision Deficiency

TypePrevalenceAffected Colors
Deuteranomaly5% of malesGreen weak
Protanomaly1% of malesRed weak
Deuteranopia1% of malesGreen blind
Protanopia1% of malesRed blind
Tritanopia0.01%Blue blind (rare)
Monochromacy0.003%No color (very rare)

Problematic Color Combinations

These pairings are hard for colorblind users:

AVOID: Red + Green       (most common confusion)
AVOID: Green + Brown
AVOID: Blue + Purple     (tritanopia)
AVOID: Green + Blue      (some types)
AVOID: Light green + Yellow
AVOID: Red + Brown

Never Rely on Color Alone

Bad: color is the only signal.

<!-- Bad: color is the only indicator -->
<span class="status-good">Online</span>
<span class="status-bad">Offline</span>

Good: color plus an icon, label, or pattern.

<!-- Good: color + icon + text -->
<span class="status-good">[OK] Online</span>
<span class="status-bad">[X] Offline</span>

<!-- For charts, use different line patterns alongside color -->

Accessible Status Indicators

/* Error: red + icon + text */
.error {
  color: hsl(0, 70%, 40%);
  border-left: 4px solid currentColor;
}
.error::before {
  content: "[!] ";  /* Icon reinforces meaning */
}

/* Success: green + icon */
.success {
  color: hsl(142, 70%, 30%);
}
.success::before {
  content: "[OK] ";
}

Charts and Data Visualization

Do not rely on color alone:

/* Use patterns, shapes, or labels */
.chart-line-1 { stroke: hsl(220, 80%, 50%); stroke-dasharray: none; }
.chart-line-2 { stroke: hsl(0, 70%, 50%); stroke-dasharray: 5,5; }
.chart-line-3 { stroke: hsl(120, 60%, 40%); stroke-dasharray: 10,3; }

/* Or label data points directly instead of relying on a legend */

Testing for Accessibility

Contrast Checking Tools

Browser extensions:

  • WAVE (Web Accessibility Evaluation Tool)
  • axe DevTools
  • Accessibility Insights

Online tools:

  • WebAIM Contrast Checker (webaim.org/resources/contrastchecker)
  • Coolors Contrast Checker
  • Stark (Figma plugin)

DevTools:

  • Chrome DevTools: Elements panel, Computed, Contrast ratio
  • Firefox: Accessibility Inspector

Color Blindness Simulation

Browser DevTools (Chrome and Edge):

  1. DevTools, Rendering tab (More Tools, Rendering)
  2. Scroll to "Emulate vision deficiencies"
  3. Select protanopia, deuteranopia, and so on

Design tools:

  • Figma: plugins like "Color Blind" or "Stark"
  • Sketch: Stark plugin

Test your designs in at least deuteranopia and protanopia. Those are the most common.

Building Accessible Color Palettes

Process

  1. Start with your brand colors.
  2. Check contrast against white and black.
  3. Adjust lightness to meet requirements.
  4. Create a light and dark variant for each.
  5. Test with colorblind simulators.

Example: Creating an Accessible Blue

/* Starting color */
--blue-500: hsl(220, 80%, 50%);  /* 4.5:1 on white, PASS */

/* For text ON blue-500, we need contrast */
/* White text on blue-500: */
/* hsl(0, 0%, 100%) on hsl(220, 80%, 50%) = 4.6:1, barely passes */

/* Safer: darken the blue for better white text */
--blue-600: hsl(220, 80%, 42%);  /* 6.5:1 white text, PASS */

Semantic Color Scale Example

:root {
  /* Primary: tested for contrast */
  --primary-50: hsl(220, 80%, 97%);    /* Backgrounds */
  --primary-100: hsl(220, 75%, 92%);
  --primary-200: hsl(220, 70%, 85%);
  --primary-300: hsl(220, 65%, 70%);
  --primary-400: hsl(220, 70%, 55%);
  --primary-500: hsl(220, 80%, 50%);   /* 4.5:1 on white, PASS */
  --primary-600: hsl(220, 85%, 40%);   /* 7:1 on white, PASS */
  --primary-700: hsl(220, 90%, 32%);
  --primary-800: hsl(220, 90%, 24%);
  --primary-900: hsl(220, 90%, 16%);

  /* Text colors */
  --text-on-primary-light: var(--primary-900); /* On 50 to 200 */
  --text-on-primary-dark: hsl(220, 20%, 98%);  /* On 500 and darker */
}

Common Accessibility Mistakes

1. Placeholder Text with Low Contrast

/* Bad: placeholder is too light */
.input::placeholder {
  color: hsl(0, 0%, 75%);  /* ~2:1 on white, FAIL */
}

/* Better, but do not depend on placeholders for meaning */
.input::placeholder {
  color: hsl(0, 0%, 50%);  /* ~5:1 on white, PASS */
}

2. Disabled States Too Faded

/* Bad: you can't tell it's a button */
.button:disabled {
  opacity: 0.3;
}

/* Better: still visible, clearly disabled */
.button:disabled {
  background: hsl(220, 10%, 85%);
  color: hsl(220, 10%, 55%);
  cursor: not-allowed;
}
/* Bad: links look like regular text */
a {
  color: inherit;
  text-decoration: none;
}

/* Better: clear link indication */
a {
  color: hsl(220, 80%, 45%);
  text-decoration: underline;
}

/* Or, if you remove the underline, use another indicator */
a {
  color: hsl(220, 80%, 45%);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
}

4. Focus States Removed

/* NEVER do this */
*:focus {
  outline: none;  /* Breaks keyboard navigation */
}

/* Instead, style it */
*:focus-visible {
  outline: 2px solid hsl(220, 80%, 50%);
  outline-offset: 2px;
}

Quick Reference: Contrast Targets

ElementMin RatioTarget
Body text4.5:17:1+
Large text (18px+)3:14.5:1+
Placeholder text4.5:1-
Links3:1 vs surrounding textUnderline too
Buttons3:1 vs background4.5:1+
Form borders3:1 vs background-
Focus indicators3:1 vs adjacent-
Icons (meaningful)3:1-
Disabled elementsNo minimumMake visually distinct

Summary

RuleImplementation
Body text: 4.5:1Dark text ~20% lightness on white
Large text: 3:1Slightly more flexibility
UI elements: 3:1Buttons, inputs, icons
Never color aloneAdd icons, text, patterns
Test colorblindDevTools → Emulate vision deficiencies
Test contrastWebAIM, DevTools, browser extensions

WCAG AA is the floor, not the ceiling. Higher contrast is almost always better.

Next: 07-color-in-ui.md covers how to apply color in real interfaces.