Files
v10/skills/implement-accessible-ui/references/anti-patterns.md
T

5.6 KiB

Accessibility Anti-Patterns

Common mistakes that harm accessibility. For correct patterns, see linked references.


Focus

Removing Focus Indicators

Never remove focus outlines without a visible replacement.

See focus.md for focus indicator requirements.

Focus Traps Without Escape

Modal contexts must allow Escape to exit. A focus trap with no exit is a keyboard trap (WCAG 2.1.2 violation).

Positive Tabindex

<!-- Never: disrupts natural tab order -->
<button tabindex="5">First</button>
<button tabindex="1">Second</button>

Use DOM order or tabindex="0". Positive values create unpredictable navigation.


ARIA

ARIA on Native Elements

<!-- Redundant -->
<button role="button">Click</button>

<!-- Unnecessary -->
<div role="button" tabindex="0">Click</div>

Native elements have implicit roles and built-in keyboard handling. See aria.md First Rule of ARIA.

Invalid ARIA Usage

<!-- aria-expanded on non-expandable -->
<input type="text" aria-expanded="false" />

<!-- aria-pressed on navigation link -->
<a href="/home" aria-pressed="false">Home</a>

<!-- aria-hidden on focusable element -->
<button aria-hidden="true">Hidden but focusable</button>

ARIA attributes must match the element's actual behavior.

Missing Required Attributes

<!-- Incomplete slider -->
<div role="slider" tabindex="0"></div>

<!-- Complete slider -->
<div role="slider" tabindex="0" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>

See aria.md for required attributes by role.

Duplicate IDs

<label id="name">Name</label>
<input aria-labelledby="name" />

<label id="name">Email</label>
<!-- Duplicate! -->
<input aria-labelledby="name" />
<!-- Points to wrong label -->

IDs must be unique. Duplicate IDs break ARIA relationships.


Keyboard

Click-Only Handlers

Interactive elements must respond to keyboard. Use native <button> or <a href> which include keyboard handling.

See keyboard.md for non-semantic element handling.

Missing Roving Tabindex

<!-- Wrong: all items in tab order -->
<div role="toolbar">
  <button tabindex="0">Bold</button>
  <button tabindex="0">Italic</button>
</div>

<!-- Correct: one tab stop, arrows navigate -->
<div role="toolbar">
  <button tabindex="0">Bold</button>
  <button tabindex="-1">Italic</button>
</div>

Composite widgets (toolbars, menus, tabs) need roving tabindex. See keyboard.md.

Conflicting Shortcuts

Avoid keys used by assistive technology:

  • Insert — NVDA/JAWS modifier
  • Caps Lock — VoiceOver modifier
  • Single letters without focus — interfere with browse mode

Use standard keys: Enter, Space, Escape, Arrow keys.


Live Regions

Assertive for Non-Critical Updates

<!-- Wrong: interrupts user -->
<div aria-live="assertive">Items loaded</div>

<!-- Correct: waits for pause -->
<div aria-live="polite">Items loaded</div>

Reserve assertive for errors and urgent alerts only.

Region Not in DOM on Load

<!-- Wrong: region added dynamically -->
<!-- (Screen readers may miss first announcement) -->

<!-- Correct: always in DOM, content changes -->
<div role="alert" aria-live="assertive"></div>

Live regions must exist in DOM before content updates.

Too Many Regions

<!-- Wrong: competing announcements -->
<div aria-live="polite">Status 1</div>
<div aria-live="polite">Status 2</div>
<div aria-live="polite">Status 3</div>

<!-- Correct: single region -->
<div aria-live="polite" aria-atomic="true"></div>

Use one live region per announcement type. Combine messages if needed.


Content

Images Without Alt Text

<!-- Missing -->
<img src="logo.png" />

<!-- Useless -->
<img src="logo.png" alt="image" />

<!-- Descriptive -->
<img src="logo.png" alt="Video.js logo" />

<!-- Decorative -->
<img src="decorative.png" alt="" />

See aria.md images section.

Empty Interactive Elements

<!-- No accessible name -->
<button><svg>...</svg></button>

<!-- Has accessible name -->
<button aria-label="Close"><svg aria-hidden="true">...</svg></button>

Every interactive element needs an accessible name.

Color as Only Indicator

<!-- Color only -->
<span style="color: red">Error</span>

<!-- Color plus text/icon -->
<span style="color: red">Error: Invalid input</span>

Information must not rely on color alone (WCAG 1.4.1).


Motion

No Reduced Motion Support

It's not always required to completely nuke transitions/animations. Sometimes we can instead reduce their duration and delay. People enabling reduced motion often still benefit from some transition - it helps orientation and avoids jarring state changes.

--transition-duration: 200ms;
--animation-duration: 200ms;

@media (prefers-reduced-motion: reduce) {
  .popup {
    --transition-duration: 50ms;
    --animation-duration: 50ms;
  }
}

Remove non-essential motion - e.g. parallax, large transforms, bouncing, zooming, auto-playing animations.

.element {
  animation: bounce 1s infinite;
}

@media (prefers-reduced-motion: reduce) {
  .element {
    animation: none;
  }
}

Always respect prefers-reduced-motion.

Auto-Playing Media

<!-- Problematic -->
<video autoplay>...</video>

<!-- Acceptable -->
<video autoplay muted>...</video>
<video controls>...</video>

Auto-playing audio disrupts screen reader users. Mute by default or require user initiation.


See Also