Visual Accessibility: Color, Contrast, and Readability

Easy

Visual accessibility makes content perceivable - essential for users with visual impairments.

Quick Decision Guide

Visual Accessibility Essentials:

1. Color contrast: 4.5:1 minimum (AA), 7:1 enhanced (AAA) 2. Don't rely on color: Use icons/labels with color 3. Resizable text: Use rem/em, support 200% zoom 4. Readable fonts: 16px minimum, good line height 5. Reduced motion: Respect prefers-reduced-motion 6. Focus visible: Clear indicators for keyboard users

Tools: WebAIM Contrast Checker, axe DevTools, Lighthouse

Result: Usable by people with low vision, color blindness, and motion sensitivity.

Color Contrast

WCAG Standards

Level AA (Minimum):
- Normal text (<18pt): 4.5:1
- Large text (≥18pt or ≥14pt bold): 3:1
- UI components: 3:1

Level AAA (Enhanced):
- Normal text: 7:1
- Large text: 4.5:1

Examples

/* ❌ Fail: 2.1:1 contrast */
.text {
  color: #999;  /* Light gray */
  background: #fff;  /* White */
}

/* ⚠️ Pass AA (4.6:1) but not AAA */
.text {
  color: #757575;  /* Medium gray */
  background: #fff;
}

/* ✅ Pass AAA (7.5:1) */
.text {
  color: #333;  /* Dark gray */
  background: #fff;
}

/* ✅ Pass AA (4.9:1) */
.text {
  color: #fff;
  background: #0070f3;  /* Blue */
}

Check All States

.button {
  color: #fff;
  background: #0070f3;  /* Check: 4.5:1 ✅ */
}

.button:hover {
  background: #0051cc;  /* Check: Still 4.5:1 ✅ */
}

.button:disabled {
  color: #666;
  background: #ccc;  /* Check: 3.5:1 ❌ - Not enough! */
}

/* Fix disabled state */
.button:disabled {
  color: #333;  /* Darker text */
  background: #e0e0e0;  /* Lighter background */
  /* Now: 4.7:1 ✅ */
}

Tools

WebAIM Contrast Checker: https://webaim.org/resources/contrastchecker/
Chrome DevTools: Inspect > Color picker shows contrast ratio
axe DevTools: Automated contrast checking

Color Independence

Don't Rely on Color Alone

// ❌ Bad: Color only
<div>
  <span style={{ color: 'red' }}>Error</span>
  <span style={{ color: 'green' }}>Success</span>
</div>

// ✅ Good: Icon + text + color
<div>
  <span style={{ color: 'red' }}>
    <ErrorIcon /> Error: Invalid input
  </span>
  <span style={{ color: 'green' }}>
    <CheckIcon /> Success: Saved
  </span>
</div>

// ❌ Bad: Color-coded chart only
<PieChart 
  data={[
    { value: 30, color: 'red' },
    { value: 70, color: 'green' }
  ]}
/>

// ✅ Good: Labels + patterns + color
<PieChart 
  data={[
    { value: 30, color: 'red', label: 'Failed: 30%', pattern: 'diagonal' },
    { value: 70, color: 'green', label: 'Passed: 70%', pattern: 'solid' }
  ]}
/>

Form Validation

// ❌ Bad: Red border only
<input 
  className={error ? 'border-red-500' : 'border-gray-300'}
/>

// ✅ Good: Border + icon + text
<div>
  <input 
    className={error ? 'border-red-500' : 'border-gray-300'}
    aria-invalid={!!error}
    aria-describedby={error ? 'error-msg' : undefined}
  />
  {error && (
    <div id="error-msg" role="alert" className="text-red-500">
      <ErrorIcon /> {error}
    </div>
  )}
</div>

Links

/* ❌ Bad: Color only, no underline */
a {
  color: blue;
  text-decoration: none;
}

/* ✅ Good: Underline or other visual indicator */
a {
  color: blue;
  text-decoration: underline;
}

/* Or use another indicator on hover/focus */
a {
  color: blue;
  text-decoration: none;
  border-bottom: 2px solid transparent;
}

a:hover,
a:focus {
  border-bottom-color: blue;
}

Text Size & Readability

Font Size

/* ✅ Use relative units */
body {
  font-size: 16px;  /* Base size (1rem) */
}

h1 { font-size: 2rem; }     /* 32px */
h2 { font-size: 1.5rem; }   /* 24px */
h3 { font-size: 1.25rem; }  /* 20px */
p { font-size: 1rem; }      /* 16px */
small { font-size: 0.875rem; } /* 14px */

/* ❌ Don't use fixed pixels for text */
.text {
  font-size: 14px;  /* Won't scale with user preferences */
}

Line Height & Spacing

/* ✅ Good readability */
p {
  font-size: 1rem;
  line-height: 1.5;  /* 150% of font size */
  max-width: 65ch;   /* 65 characters per line */
  margin-bottom: 1em;
}

/* ❌ Too tight */
p {
  line-height: 1;  /* Hard to read */
}

/* ✅ Spacing for lists */
ul, ol {
  line-height: 1.5;
}

li {
  margin-bottom: 0.5em;
}

Support Text Zoom

/* ❌ Fixed heights break on zoom */
.container {
  height: 400px;
  overflow: hidden;  /* Text gets cut off */
}

/* ✅ Flexible heights */
.container {
  min-height: 400px;
  height: auto;
}

/* ❌ Fixed widths break layout */
.sidebar {
  width: 200px;
}

/* ✅ Use responsive units */
.sidebar {
  width: min(200px, 30vw);
  min-width: 150px;
}

Font Choices

/* ✅ Readable font families */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 
               'Inter', 'Helvetica', 'Arial', sans-serif;
}

/* Avoid: */
/* - Decorative fonts for body text */
/* - All caps for long text */
/* - Low contrast fonts */

Motion & Animation

Respect User Preferences

/* Reduce motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Safe Animation Patterns

// Check user preference
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

// Conditional animation
function AnimatedComponent() {
  return (
    <motion.div
      initial={{ opacity: 0, y: prefersReducedMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ 
        duration: prefersReducedMotion ? 0 : 0.3 
      }}
    >
      Content
    </motion.div>
  );
}

// Or disable entirely
function Component() {
  if (prefersReducedMotion) {
    return <div>Content</div>;
  }
  
  return <AnimatedDiv>Content</AnimatedDiv>;
}

Auto-Playing Content

// ❌ Bad: Auto-play video
<video src="video.mp4" autoPlay />

// ✅ Good: User-initiated
<video src="video.mp4" controls />

// ✅ Good: Pause button if auto-playing
function VideoPlayer() {
  const [playing, setPlaying] = useState(!prefersReducedMotion);
  
  return (
    <div>
      <video 
        src="video.mp4" 
        autoPlay={playing}
        muted
      />
      <button onClick={() => setPlaying(!playing)}>
        {playing ? 'Pause' : 'Play'}
      </button>
    </div>
  );
}

Avoid Seizure Triggers

No more than 3 flashes per second
Avoid large flashing areas
Provide warnings before flashing content
// Warning before flashing content
<div>
  <div role="alert">
    ⚠️ Warning: This video contains flashing lights
  </div>
  <button onClick={() => setShowVideo(true)}>
    I understand, show video
  </button>
</div>

Focus Indicators & Interactive Elements

Visible Focus

/* ❌ Never remove focus outline */
button:focus {
  outline: none;  /* NEVER! */
}

/* ✅ Custom but visible focus */
button:focus-visible {
  outline: 2px solid #0070f3;
  outline-offset: 2px;
}

/* High contrast mode support */
@media (prefers-contrast: high) {
  button:focus-visible {
    outline: 3px solid currentColor;
  }
}

/* Dark theme support */
@media (prefers-color-scheme: dark) {
  button:focus-visible {
    outline-color: #66b3ff;
  }
}

Touch Targets

/* ✅ Minimum 44x44px touch targets */
button,
a {
  min-width: 44px;
  min-height: 44px;
  padding: 0.5rem 1rem;
}

/* Ensure spacing between targets */
.button-group button {
  margin-right: 0.5rem;
}

Hover & Focus States

/* ✅ Visible states */
button {
  background: #0070f3;
  transition: background 0.2s;
}

button:hover {
  background: #0051cc;
}

button:focus-visible {
  outline: 2px solid #0070f3;
  outline-offset: 2px;
}

button:active {
  background: #003d99;
}

button:disabled {
  background: #ccc;
  cursor: not-allowed;
  opacity: 0.6;
}

Testing & Tools

Browser DevTools

Chrome DevTools:

1. Lighthouse audit (Accessibility score)

2. Inspect > Color picker (shows contrast ratio)

3. Rendering tab > Emulate vision deficiencies

Firefox:

Accessibility Inspector
Color contrast checker

Browser Extensions

axe DevTools: Comprehensive accessibility testing
WAVE: Visual feedback overlays
Color Contrast Analyzer: Real-time contrast checking

Manual Testing

Zoom to 200%

Is text still readable?
Does layout break?
Can you still access all features?

Test with different color schemes

Light mode
Dark mode
High contrast mode

Simulate color blindness

Chrome: DevTools > Rendering > Emulate vision deficiencies
Try protanopia, deuteranopia, tritanopia

Keyboard navigation

Is focus visible?
Can you reach all interactive elements?

Common Issues Checklist

❌ Low color contrast

❌ Text not resizable

❌ Color as only indicator

❌ No focus indicator

❌ Auto-playing animations

❌ Small touch targets

❌ Fixed text sizes

❌ Missing alt text