Contrast and Accessibility

Accessible color choices ensure your designs work for everyone, including the 300+ million people with color vision deficiencies. This isn't optional - it's 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 lighter color
      L2 = luminance of darker color
      Luminance ranges from 0 (black) to 1 (white)

You don't need to calculate manually - use tools (covered later).

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

Recommendation: Target AA for everything, AAA for critical content.

Practical Contrast Guidelines

Text on Backgrounds

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

/* 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 ✗ */
}

Text Color Recommendations

For light backgrounds (white/light gray):

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

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

  /* 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 ✓ */
  --text-secondary-on-dark: hsl(220, 10%, 70%); /* ~6:1 ✓ */
}

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 ✓ */
}

/* Focus indicator needs 3:1 against both the element AND 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 ✓ */
}

/* Or use 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 combinations are difficult for colorblind users:

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

Never Rely on Color Alone

Bad: Using only color to convey meaning

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

Good: Use color + another indicator (icon, text, pattern)

<!-- Good: Color + icon + text -->
<span class="status-good">✓ Online</span>
<span class="status-bad">✗ Offline</span>

<!-- Good: Color + pattern in charts -->
<!-- Use different line patterns, not just colors -->

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: "✓ ";
}

Charts and Data Visualization

Don't 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 use direct labels instead of legends */

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/Edge):

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

Design Tools:

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

Test your designs in at least deuteranopia and protanopia (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 ✓ */

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

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

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 ✓ */
  --primary-600: hsl(220, 85%, 40%);  /* 7:1 on white ✓ */
  --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-200 */
  --text-on-primary-dark: hsl(220, 20%, 98%);  /* On 500+ */
}

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 ✗ */
}

/* Better - But placeholders shouldn't be relied on */
.input::placeholder {
  color: hsl(0, 0%, 50%);  /* ~5:1 on white ✓ */
}

2. Disabled States Too Faded

/* Bad - Can't see 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 removing 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, customize 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

The accessibility floor, not ceiling: Meeting WCAG AA is the minimum. Higher contrast is almost always better.

Next: 07-color-in-ui.md - Applying color to user interfaces