antipatterns.md

Antipatterns

Common accessibility mistakes and how to fix them.

Contents

Div soup

Wrong:

<div class="nav">
  <div class="nav-item" onclick="goto('/')">Home</div>
  <div class="nav-item" onclick="goto('/about')">About</div>
</div>

Right:

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>

The div version requires role="navigation", role="link", tabindex="0", keyboard handlers, and focus styles. The semantic version works out of the box.

ARIA overuse

Wrong:

<button role="button" aria-label="Submit form" tabindex="0">
  Submit form
</button>

Right:

<button type="submit">Submit form</button>

Problems with the wrong version:

  • role="button" is redundant on <button>
  • aria-label duplicates visible text (use only when no visible text)
  • tabindex="0" is redundant on <button>

Rule: If a native HTML element does what you need, use it without ARIA.

Placeholder as label

Wrong:

<input type="email" placeholder="Email address">

Right:

<label for="email">Email address</label>
<input type="email" id="email" placeholder="e.g. user@example.com">

Problems with placeholder-only:

  • Disappears when typing (users forget what field is for)
  • Low contrast by default (hard to read)
  • Not announced as field name by all screen readers
  • Placeholder is hint text, not label

Empty or meaningless alt text

Wrong:

<img src="graph.png" alt="">
<img src="graph.png" alt="image">
<img src="graph.png" alt="graph.png">

Right:

<img src="graph.png" alt="Line graph showing revenue growth from $1M to $3M over 2024">
  • alt="" marks image as decorative - only use for truly decorative images
  • "image", "photo", "graph" are meaningless - describe the content
  • Filenames are not descriptions

For complex images: Provide brief alt + detailed description nearby:

<figure>
  <img src="flowchart.png" alt="Application deployment workflow">
  <figcaption>
    <details>
      <summary>Flowchart description</summary>
      <p>1. Developer pushes to main branch. 2. CI runs tests...</p>
    </details>
  </figcaption>
</figure>

Removing focus indicators

Wrong:

*:focus {
  outline: none;
}

button:focus {
  outline: 0;
}

Right:

button:focus {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

/* If you must customize, provide visible alternative */
button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
}

Focus indicators tell keyboard users where they are. Removing them makes the site unusable for keyboard navigation.

Mouse-only interactions

Wrong:

<div 
  onMouseEnter={() => setOpen(true)}
  onMouseLeave={() => setOpen(false)}
>
  <span>Hover for menu</span>
  {open && <Menu />}
</div>

Right:

<button
  onClick={() => setOpen(!open)}
  onKeyDown={(e) => e.key === 'Escape' && setOpen(false)}
  aria-expanded={open}
  aria-haspopup="menu"
>
  Menu
</button>
{open && <Menu onClose={() => setOpen(false)} />}
  • Hover doesn't exist on touch devices
  • Keyboard users can't trigger hover
  • Click/keyboard activation works universally

Auto-playing media

Wrong:

<video autoplay>
  <source src="intro.mp4" type="video/mp4">
</video>

Right:

<video controls>
  <source src="intro.mp4" type="video/mp4">
  <track kind="captions" src="intro.vtt" srclang="en" label="English">
</video>
  • Auto-play disrupts screen readers
  • Users with vestibular disorders need motion control
  • Background audio conflicts with screen reader audio
  • Always provide captions for video with speech

If auto-play is required:

<video autoplay muted>

And provide visible pause control.

Ignoring motion preferences

Wrong:

.hero {
  animation: parallax 2s infinite;
}

.button {
  transition: all 0.5s ease;
}

Right:

.hero {
  animation: parallax 2s infinite;
}

.button {
  transition: all 0.5s ease;
}

@media (prefers-reduced-motion: reduce) {
  .hero {
    animation: none;
  }
  
  .button {
    transition: none;
  }
}
  • Animations can trigger vestibular disorders (vertigo, nausea)
  • ~35% of adults over 40 have vestibular dysfunction
  • Check prefers-reduced-motion for: parallax, transitions, auto-playing animations, smooth scrolling
  • "Reduce" doesn't mean "remove all" - subtle opacity fades are usually fine

Color as only indicator

Wrong:

<span class="status-good">Available</span>  <!-- green -->
<span class="status-bad">Unavailable</span> <!-- red -->

Right:

<span class="status-good">✓ Available</span>
<span class="status-bad">✗ Unavailable</span>

<!-- Or with icons -->
<span class="status-good">
  <svg aria-hidden="true"><!-- checkmark --></svg>
  Available
</span>
  • ~8% of men have color vision deficiency
  • Color alone doesn't convey meaning to screen readers
  • Use text, icons, or patterns in addition to color