1# Antipatterns
2
3Common accessibility mistakes and how to fix them.
4
5## Contents
6- [Div soup](#div-soup)
7- [ARIA overuse](#aria-overuse)
8- [Placeholder as label](#placeholder-as-label)
9- [Empty or meaningless alt text](#empty-or-meaningless-alt-text)
10- [Removing focus indicators](#removing-focus-indicators)
11- [Mouse-only interactions](#mouse-only-interactions)
12- [Auto-playing media](#auto-playing-media)
13- [Ignoring motion preferences](#ignoring-motion-preferences)
14- [Color as only indicator](#color-as-only-indicator)
15
16## Div soup
17
18**Wrong:**
19```html
20<div class="nav">
21 <div class="nav-item" onclick="goto('/')">Home</div>
22 <div class="nav-item" onclick="goto('/about')">About</div>
23</div>
24```
25
26**Right:**
27```html
28<nav>
29 <a href="/">Home</a>
30 <a href="/about">About</a>
31</nav>
32```
33
34The div version requires `role="navigation"`, `role="link"`, `tabindex="0"`, keyboard handlers, and focus styles. The semantic version works out of the box.
35
36## ARIA overuse
37
38**Wrong:**
39```html
40<button role="button" aria-label="Submit form" tabindex="0">
41 Submit form
42</button>
43```
44
45**Right:**
46```html
47<button type="submit">Submit form</button>
48```
49
50Problems with the wrong version:
51- `role="button"` is redundant on `<button>`
52- `aria-label` duplicates visible text (use only when no visible text)
53- `tabindex="0"` is redundant on `<button>`
54
55**Rule:** If a native HTML element does what you need, use it without ARIA.
56
57## Placeholder as label
58
59**Wrong:**
60```html
61<input type="email" placeholder="Email address">
62```
63
64**Right:**
65```html
66<label for="email">Email address</label>
67<input type="email" id="email" placeholder="e.g. user@example.com">
68```
69
70Problems with placeholder-only:
71- Disappears when typing (users forget what field is for)
72- Low contrast by default (hard to read)
73- Not announced as field name by all screen readers
74- Placeholder is hint text, not label
75
76## Empty or meaningless alt text
77
78**Wrong:**
79```html
80<img src="graph.png" alt="">
81<img src="graph.png" alt="image">
82<img src="graph.png" alt="graph.png">
83```
84
85**Right:**
86```html
87<img src="graph.png" alt="Line graph showing revenue growth from $1M to $3M over 2024">
88```
89
90- `alt=""` marks image as decorative - only use for truly decorative images
91- "image", "photo", "graph" are meaningless - describe the content
92- Filenames are not descriptions
93
94**For complex images:** Provide brief alt + detailed description nearby:
95```html
96<figure>
97 <img src="flowchart.png" alt="Application deployment workflow">
98 <figcaption>
99 <details>
100 <summary>Flowchart description</summary>
101 <p>1. Developer pushes to main branch. 2. CI runs tests...</p>
102 </details>
103 </figcaption>
104</figure>
105```
106
107## Removing focus indicators
108
109**Wrong:**
110```css
111*:focus {
112 outline: none;
113}
114
115button:focus {
116 outline: 0;
117}
118```
119
120**Right:**
121```css
122button:focus {
123 outline: 2px solid #005fcc;
124 outline-offset: 2px;
125}
126
127/* If you must customize, provide visible alternative */
128button:focus-visible {
129 outline: none;
130 box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
131}
132```
133
134Focus indicators tell keyboard users where they are. Removing them makes the site unusable for keyboard navigation.
135
136## Mouse-only interactions
137
138**Wrong:**
139```jsx
140<div
141 onMouseEnter={() => setOpen(true)}
142 onMouseLeave={() => setOpen(false)}
143>
144 <span>Hover for menu</span>
145 {open && <Menu />}
146</div>
147```
148
149**Right:**
150```jsx
151<button
152 onClick={() => setOpen(!open)}
153 onKeyDown={(e) => e.key === 'Escape' && setOpen(false)}
154 aria-expanded={open}
155 aria-haspopup="menu"
156>
157 Menu
158</button>
159{open && <Menu onClose={() => setOpen(false)} />}
160```
161
162- Hover doesn't exist on touch devices
163- Keyboard users can't trigger hover
164- Click/keyboard activation works universally
165
166## Auto-playing media
167
168**Wrong:**
169```html
170<video autoplay>
171 <source src="intro.mp4" type="video/mp4">
172</video>
173```
174
175**Right:**
176```html
177<video controls>
178 <source src="intro.mp4" type="video/mp4">
179 <track kind="captions" src="intro.vtt" srclang="en" label="English">
180</video>
181```
182
183- Auto-play disrupts screen readers
184- Users with vestibular disorders need motion control
185- Background audio conflicts with screen reader audio
186- Always provide captions for video with speech
187
188If auto-play is required:
189```html
190<video autoplay muted>
191```
192And provide visible pause control.
193
194## Ignoring motion preferences
195
196**Wrong:**
197```css
198.hero {
199 animation: parallax 2s infinite;
200}
201
202.button {
203 transition: all 0.5s ease;
204}
205```
206
207**Right:**
208```css
209.hero {
210 animation: parallax 2s infinite;
211}
212
213.button {
214 transition: all 0.5s ease;
215}
216
217@media (prefers-reduced-motion: reduce) {
218 .hero {
219 animation: none;
220 }
221
222 .button {
223 transition: none;
224 }
225}
226```
227
228- Animations can trigger vestibular disorders (vertigo, nausea)
229- ~35% of adults over 40 have vestibular dysfunction
230- Check `prefers-reduced-motion` for: parallax, transitions, auto-playing animations, smooth scrolling
231- "Reduce" doesn't mean "remove all" - subtle opacity fades are usually fine
232
233## Color as only indicator
234
235**Wrong:**
236```html
237<span class="status-good">Available</span> <!-- green -->
238<span class="status-bad">Unavailable</span> <!-- red -->
239```
240
241**Right:**
242```html
243<span class="status-good">✓ Available</span>
244<span class="status-bad">✗ Unavailable</span>
245
246<!-- Or with icons -->
247<span class="status-good">
248 <svg aria-hidden="true"><!-- checkmark --></svg>
249 Available
250</span>
251```
252
253- ~8% of men have color vision deficiency
254- Color alone doesn't convey meaning to screen readers
255- Use text, icons, or patterns in addition to color