codingwithagent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/bin/init.js +257 -0
- package/package.json +56 -0
- package/templates/accessibility/.cursorrules +342 -0
- package/templates/accessibility/README.md +47 -0
- package/templates/antigravity/accessibility/.agent/rules/accessibility.md +501 -0
- package/templates/antigravity/accessibility/.agent/rules/aria-patterns.md +568 -0
- package/templates/antigravity/accessibility/.agent/rules/wcag-standard.md +225 -0
- package/templates/antigravity/accessibility/README.md +42 -0
- package/templates/antigravity/minimal/.agent/rules/accessibility.md +53 -0
- package/templates/antigravity/minimal/.agent/rules/code-quality.md +86 -0
- package/templates/antigravity/minimal/.agent/rules/react-components.md +164 -0
- package/templates/antigravity/minimal/README.md +34 -0
- package/templates/antigravity/standard/.agent/rules/accessibility.md +98 -0
- package/templates/antigravity/standard/.agent/rules/code-quality.md +166 -0
- package/templates/antigravity/standard/.agent/rules/pull-request-review.md +192 -0
- package/templates/antigravity/standard/.agent/rules/react-components.md +204 -0
- package/templates/antigravity/standard/.agent/rules/testing.md +197 -0
- package/templates/antigravity/standard/README.md +39 -0
- package/templates/antigravity/strict/.agent/README.md +46 -0
- package/templates/antigravity/strict/.agent/rules/accessibility.md +199 -0
- package/templates/antigravity/strict/.agent/rules/code-quality.md +268 -0
- package/templates/antigravity/strict/.agent/rules/pull-request-review.md +114 -0
- package/templates/antigravity/strict/.agent/rules/react-components.md +423 -0
- package/templates/antigravity/strict/.agent/rules/security.md +483 -0
- package/templates/antigravity/strict/.agent/rules/testing.md +280 -0
- package/templates/minimal/.cursorrules +48 -0
- package/templates/minimal/README.md +40 -0
- package/templates/standard/.cursorrules +184 -0
- package/templates/standard/README.md +43 -0
- package/templates/strict/.cursorrules +227 -0
- package/templates/strict/README.md +47 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# ARIA Patterns Agent Rules
|
|
6
|
+
|
|
7
|
+
## Core Directive: Semantic HTML First
|
|
8
|
+
|
|
9
|
+
**Your primary rule:** Use native HTML elements. Only add ARIA when semantic HTML cannot achieve the requirement.
|
|
10
|
+
|
|
11
|
+
**Decision flow:**
|
|
12
|
+
|
|
13
|
+
1. Can I use native HTML? (Usually YES) → Use it, no ARIA needed
|
|
14
|
+
2. Does native HTML need enhancement? → Add minimal ARIA
|
|
15
|
+
3. Is this a complex custom widget? → Use full ARIA pattern
|
|
16
|
+
|
|
17
|
+
## The First Rule of ARIA
|
|
18
|
+
|
|
19
|
+
**NO ARIA IS BETTER THAN BAD ARIA**
|
|
20
|
+
|
|
21
|
+
Never add ARIA "just in case." Each ARIA attribute must serve a specific, necessary purpose.
|
|
22
|
+
|
|
23
|
+
## Critical ARIA Attributes - When and How
|
|
24
|
+
|
|
25
|
+
### aria-label
|
|
26
|
+
|
|
27
|
+
**Use when:** No visible label exists (icon buttons, landmark regions)
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
// ✅ GENERATE: Icon-only button
|
|
31
|
+
<button aria-label="Close dialog">
|
|
32
|
+
<X aria-hidden="true" />
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
// ✅ GENERATE: Distinguish multiple nav landmarks
|
|
36
|
+
<nav aria-label="Main navigation">...</nav>
|
|
37
|
+
<nav aria-label="Footer navigation">...</nav>
|
|
38
|
+
|
|
39
|
+
// ❌ NEVER: Redundant with visible text
|
|
40
|
+
<button aria-label="Submit">Submit</button>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### aria-labelledby
|
|
44
|
+
|
|
45
|
+
**Use when:** Visible label exists elsewhere in DOM
|
|
46
|
+
|
|
47
|
+
```jsx
|
|
48
|
+
// ✅ GENERATE: Dialog with visible heading
|
|
49
|
+
<h2 id="dialog-title">Confirm Action</h2>
|
|
50
|
+
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
|
|
51
|
+
{/* content */}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
// ✅ GENERATE: Multiple labels (space-separated)
|
|
55
|
+
<div aria-labelledby="title subtitle">
|
|
56
|
+
<h2 id="title">Warning</h2>
|
|
57
|
+
<p id="subtitle">Cannot be undone</p>
|
|
58
|
+
</div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Rule:** Always generate unique IDs. Use this over `aria-label` when visible text exists.
|
|
62
|
+
|
|
63
|
+
### aria-describedby
|
|
64
|
+
|
|
65
|
+
**Use when:** Additional help text or error messages exist
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
// ✅ GENERATE: Form help text
|
|
69
|
+
<label htmlFor="password">Password</label>
|
|
70
|
+
<input
|
|
71
|
+
id="password"
|
|
72
|
+
type="password"
|
|
73
|
+
aria-describedby="password-help"
|
|
74
|
+
aria-required="true"
|
|
75
|
+
/>
|
|
76
|
+
<p id="password-help">
|
|
77
|
+
Must be 12+ characters with numbers and symbols
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
// ✅ GENERATE: Error message
|
|
81
|
+
<input
|
|
82
|
+
aria-invalid={hasError}
|
|
83
|
+
aria-describedby={hasError ? "email-error" : undefined}
|
|
84
|
+
/>
|
|
85
|
+
{hasError && (
|
|
86
|
+
<p id="email-error" role="alert">
|
|
87
|
+
Please enter valid email
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### aria-hidden
|
|
93
|
+
|
|
94
|
+
**Use when:** Hiding purely decorative elements
|
|
95
|
+
|
|
96
|
+
```jsx
|
|
97
|
+
// ✅ GENERATE: Decorative icon
|
|
98
|
+
<button>
|
|
99
|
+
Save
|
|
100
|
+
<SaveIcon aria-hidden="true" />
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
// ❌ NEVER: On interactive elements
|
|
104
|
+
<button aria-hidden="true">Click</button>
|
|
105
|
+
|
|
106
|
+
// ❌ NEVER: On important content
|
|
107
|
+
<p aria-hidden="true">Error: Failed</p>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**CRITICAL:** Never hide interactive elements or important information.
|
|
111
|
+
|
|
112
|
+
### aria-live
|
|
113
|
+
|
|
114
|
+
**Use when:** Announcing dynamic content changes
|
|
115
|
+
|
|
116
|
+
```jsx
|
|
117
|
+
// ✅ GENERATE: Status (polite - most common)
|
|
118
|
+
<div role="status" aria-live="polite" aria-atomic="true">
|
|
119
|
+
{isLoading ? 'Loading...' : `${count} results`}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
// ✅ GENERATE: Critical error (assertive - rare)
|
|
123
|
+
<div role="alert" aria-live="assertive">
|
|
124
|
+
Error: Payment failed
|
|
125
|
+
</div>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Rules:**
|
|
129
|
+
|
|
130
|
+
- `polite` for status updates, non-critical notifications
|
|
131
|
+
- `assertive` ONLY for critical errors, urgent alerts
|
|
132
|
+
- Include `aria-atomic="true"` for complete announcements
|
|
133
|
+
|
|
134
|
+
### aria-expanded
|
|
135
|
+
|
|
136
|
+
**Use when:** Content is collapsible/expandable
|
|
137
|
+
|
|
138
|
+
```jsx
|
|
139
|
+
// ✅ GENERATE: Dropdown
|
|
140
|
+
<button
|
|
141
|
+
aria-expanded={isOpen}
|
|
142
|
+
aria-controls="menu"
|
|
143
|
+
aria-haspopup="true"
|
|
144
|
+
>
|
|
145
|
+
Menu
|
|
146
|
+
</button>
|
|
147
|
+
<ul id="menu" hidden={!isOpen}>...</ul>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Rules:** Always boolean, pair with `aria-controls`, sync with `hidden` attribute.
|
|
151
|
+
|
|
152
|
+
### aria-invalid & aria-required
|
|
153
|
+
|
|
154
|
+
**Use when:** Form validation
|
|
155
|
+
|
|
156
|
+
```jsx
|
|
157
|
+
// ✅ GENERATE: Both HTML and ARIA
|
|
158
|
+
<label htmlFor="email">
|
|
159
|
+
Email <span aria-hidden="true">*</span>
|
|
160
|
+
</label>
|
|
161
|
+
<input
|
|
162
|
+
id="email"
|
|
163
|
+
required
|
|
164
|
+
aria-required="true"
|
|
165
|
+
aria-invalid={hasError}
|
|
166
|
+
aria-describedby={hasError ? "error" : undefined}
|
|
167
|
+
/>
|
|
168
|
+
{hasError && (
|
|
169
|
+
<p id="error" role="alert">Enter valid email</p>
|
|
170
|
+
)}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### aria-current
|
|
174
|
+
|
|
175
|
+
**Use when:** Indicating current item in navigation
|
|
176
|
+
|
|
177
|
+
```jsx
|
|
178
|
+
// ✅ GENERATE: Current page
|
|
179
|
+
<nav aria-label="Main navigation">
|
|
180
|
+
<a href="/" aria-current={isHome ? "page" : undefined}>
|
|
181
|
+
Home
|
|
182
|
+
</a>
|
|
183
|
+
</nav>
|
|
184
|
+
|
|
185
|
+
// ✅ GENERATE: Current step
|
|
186
|
+
<li aria-current={step === 1 ? "step" : undefined}>
|
|
187
|
+
Account Info
|
|
188
|
+
</li>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Values:** `page`, `step`, `location`, `date`, `time`, `true`
|
|
192
|
+
|
|
193
|
+
## Common Widget Patterns
|
|
194
|
+
|
|
195
|
+
### Button (Use Native!)
|
|
196
|
+
|
|
197
|
+
```jsx
|
|
198
|
+
// ✅ ALWAYS GENERATE
|
|
199
|
+
<button onClick={handleClick}>Click Me</button>
|
|
200
|
+
|
|
201
|
+
// ✅ Icon button
|
|
202
|
+
<button aria-label="Delete">
|
|
203
|
+
<TrashIcon aria-hidden="true" />
|
|
204
|
+
</button>
|
|
205
|
+
|
|
206
|
+
// ⚠️ ONLY IF ABSOLUTELY NECESSARY
|
|
207
|
+
<div
|
|
208
|
+
role="button"
|
|
209
|
+
tabIndex={0}
|
|
210
|
+
onClick={handleClick}
|
|
211
|
+
onKeyDown={(e) => {
|
|
212
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
handleClick();
|
|
215
|
+
}
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
Custom Button
|
|
219
|
+
</div>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Modal/Dialog
|
|
223
|
+
|
|
224
|
+
```jsx
|
|
225
|
+
// ✅ COMPLETE PATTERN
|
|
226
|
+
const Modal = ({ isOpen, onClose, title, children }) => {
|
|
227
|
+
const dialogRef = useRef(null);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (isOpen) {
|
|
231
|
+
dialogRef.current?.focus();
|
|
232
|
+
}
|
|
233
|
+
}, [isOpen]);
|
|
234
|
+
|
|
235
|
+
if (!isOpen) return null;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
role="dialog"
|
|
240
|
+
aria-modal="true"
|
|
241
|
+
aria-labelledby="dialog-title"
|
|
242
|
+
ref={dialogRef}
|
|
243
|
+
tabIndex={-1}
|
|
244
|
+
onKeyDown={(e) => e.key === "Escape" && onClose()}
|
|
245
|
+
>
|
|
246
|
+
<h2 id="dialog-title">{title}</h2>
|
|
247
|
+
{children}
|
|
248
|
+
<button onClick={onClose} aria-label="Close">
|
|
249
|
+
<X aria-hidden="true" />
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Required:** `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus management, Escape key.
|
|
257
|
+
|
|
258
|
+
### Tabs
|
|
259
|
+
|
|
260
|
+
```jsx
|
|
261
|
+
// ✅ FULL PATTERN WITH KEYBOARD NAV
|
|
262
|
+
const Tabs = ({ tabs }) => {
|
|
263
|
+
const [selected, setSelected] = useState(0);
|
|
264
|
+
const tabRefs = useRef([]);
|
|
265
|
+
|
|
266
|
+
const handleKeyDown = (e, index) => {
|
|
267
|
+
let newIndex = index;
|
|
268
|
+
if (e.key === "ArrowRight") newIndex = (index + 1) % tabs.length;
|
|
269
|
+
if (e.key === "ArrowLeft")
|
|
270
|
+
newIndex = (index - 1 + tabs.length) % tabs.length;
|
|
271
|
+
if (e.key === "Home") newIndex = 0;
|
|
272
|
+
if (e.key === "End") newIndex = tabs.length - 1;
|
|
273
|
+
|
|
274
|
+
if (newIndex !== index) {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
setSelected(newIndex);
|
|
277
|
+
tabRefs.current[newIndex]?.focus();
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<>
|
|
283
|
+
<div role="tablist" aria-label="Content tabs">
|
|
284
|
+
{tabs.map((tab, i) => (
|
|
285
|
+
<button
|
|
286
|
+
key={i}
|
|
287
|
+
ref={(el) => (tabRefs.current[i] = el)}
|
|
288
|
+
role="tab"
|
|
289
|
+
aria-selected={selected === i}
|
|
290
|
+
aria-controls={`panel-${i}`}
|
|
291
|
+
id={`tab-${i}`}
|
|
292
|
+
tabIndex={selected === i ? 0 : -1}
|
|
293
|
+
onClick={() => setSelected(i)}
|
|
294
|
+
onKeyDown={(e) => handleKeyDown(e, i)}
|
|
295
|
+
>
|
|
296
|
+
{tab.label}
|
|
297
|
+
</button>
|
|
298
|
+
))}
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{tabs.map((tab, i) => (
|
|
302
|
+
<div
|
|
303
|
+
key={i}
|
|
304
|
+
role="tabpanel"
|
|
305
|
+
id={`panel-${i}`}
|
|
306
|
+
aria-labelledby={`tab-${i}`}
|
|
307
|
+
hidden={selected !== i}
|
|
308
|
+
>
|
|
309
|
+
{tab.content}
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
</>
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Required:** Arrow navigation, Home/End keys, roving tabindex.
|
|
318
|
+
|
|
319
|
+
### Accordion
|
|
320
|
+
|
|
321
|
+
```jsx
|
|
322
|
+
// ✅ GENERATE THIS
|
|
323
|
+
const Accordion = ({ title, content, isOpen, onToggle }) => {
|
|
324
|
+
const headingId = `heading-${useId()}`;
|
|
325
|
+
const panelId = `panel-${useId()}`;
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<div>
|
|
329
|
+
<h3 id={headingId}>
|
|
330
|
+
<button
|
|
331
|
+
aria-expanded={isOpen}
|
|
332
|
+
aria-controls={panelId}
|
|
333
|
+
onClick={onToggle}
|
|
334
|
+
>
|
|
335
|
+
{title}
|
|
336
|
+
</button>
|
|
337
|
+
</h3>
|
|
338
|
+
<div
|
|
339
|
+
id={panelId}
|
|
340
|
+
role="region"
|
|
341
|
+
aria-labelledby={headingId}
|
|
342
|
+
hidden={!isOpen}
|
|
343
|
+
>
|
|
344
|
+
{content}
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Combobox (Autocomplete)
|
|
352
|
+
|
|
353
|
+
```jsx
|
|
354
|
+
// ✅ SEARCHABLE DROPDOWN
|
|
355
|
+
const Combobox = ({ options, value, onChange }) => {
|
|
356
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
357
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
358
|
+
|
|
359
|
+
const handleKeyDown = (e) => {
|
|
360
|
+
if (e.key === "ArrowDown") {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
setIsOpen(true);
|
|
363
|
+
setActiveIndex((prev) => Math.min(prev + 1, options.length - 1));
|
|
364
|
+
}
|
|
365
|
+
if (e.key === "ArrowUp") {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
setActiveIndex((prev) => Math.max(prev - 1, 0));
|
|
368
|
+
}
|
|
369
|
+
if (e.key === "Enter" && activeIndex >= 0) {
|
|
370
|
+
onChange(options[activeIndex]);
|
|
371
|
+
setIsOpen(false);
|
|
372
|
+
}
|
|
373
|
+
if (e.key === "Escape") {
|
|
374
|
+
setIsOpen(false);
|
|
375
|
+
setActiveIndex(-1);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<>
|
|
381
|
+
<input
|
|
382
|
+
role="combobox"
|
|
383
|
+
aria-expanded={isOpen}
|
|
384
|
+
aria-controls="listbox"
|
|
385
|
+
aria-activedescendant={
|
|
386
|
+
activeIndex >= 0 ? `option-${activeIndex}` : undefined
|
|
387
|
+
}
|
|
388
|
+
aria-autocomplete="list"
|
|
389
|
+
value={value}
|
|
390
|
+
onChange={(e) => {
|
|
391
|
+
onChange(e.target.value);
|
|
392
|
+
setIsOpen(true);
|
|
393
|
+
}}
|
|
394
|
+
onKeyDown={handleKeyDown}
|
|
395
|
+
/>
|
|
396
|
+
{isOpen && (
|
|
397
|
+
<ul id="listbox" role="listbox">
|
|
398
|
+
{options.map((opt, i) => (
|
|
399
|
+
<li
|
|
400
|
+
key={i}
|
|
401
|
+
id={`option-${i}`}
|
|
402
|
+
role="option"
|
|
403
|
+
aria-selected={i === activeIndex}
|
|
404
|
+
>
|
|
405
|
+
{opt}
|
|
406
|
+
</li>
|
|
407
|
+
))}
|
|
408
|
+
</ul>
|
|
409
|
+
)}
|
|
410
|
+
</>
|
|
411
|
+
);
|
|
412
|
+
};
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Alerts & Status
|
|
416
|
+
|
|
417
|
+
```jsx
|
|
418
|
+
// ✅ Success (polite)
|
|
419
|
+
<div role="status" aria-live="polite">
|
|
420
|
+
<CheckIcon aria-hidden="true" />
|
|
421
|
+
Saved successfully
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
// ✅ Error (assertive)
|
|
425
|
+
<div role="alert" aria-live="assertive">
|
|
426
|
+
<ErrorIcon aria-hidden="true" />
|
|
427
|
+
Payment failed
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
// ✅ Loading
|
|
431
|
+
<div role="status" aria-live="polite" aria-busy="true">
|
|
432
|
+
<Spinner aria-hidden="true" />
|
|
433
|
+
Loading...
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
// ✅ Progress
|
|
437
|
+
<div
|
|
438
|
+
role="progressbar"
|
|
439
|
+
aria-valuenow={50}
|
|
440
|
+
aria-valuemin={0}
|
|
441
|
+
aria-valuemax={100}
|
|
442
|
+
>
|
|
443
|
+
50% complete
|
|
444
|
+
</div>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Breadcrumbs
|
|
448
|
+
|
|
449
|
+
```jsx
|
|
450
|
+
// ✅ NAVIGATION BREADCRUMBS
|
|
451
|
+
<nav aria-label="Breadcrumb">
|
|
452
|
+
<ol>
|
|
453
|
+
<li>
|
|
454
|
+
<a href="/">Home</a>
|
|
455
|
+
</li>
|
|
456
|
+
<li>
|
|
457
|
+
<a href="/products">Products</a>
|
|
458
|
+
</li>
|
|
459
|
+
<li aria-current="page">Laptop</li>
|
|
460
|
+
</ol>
|
|
461
|
+
</nav>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Tooltip
|
|
465
|
+
|
|
466
|
+
```jsx
|
|
467
|
+
// ✅ ACCESSIBLE TOOLTIP
|
|
468
|
+
const Tooltip = ({ children, text }) => {
|
|
469
|
+
const [show, setShow] = useState(false);
|
|
470
|
+
const id = useId();
|
|
471
|
+
|
|
472
|
+
return (
|
|
473
|
+
<>
|
|
474
|
+
<button
|
|
475
|
+
aria-describedby={show ? id : undefined}
|
|
476
|
+
onMouseEnter={() => setShow(true)}
|
|
477
|
+
onMouseLeave={() => setShow(false)}
|
|
478
|
+
onFocus={() => setShow(true)}
|
|
479
|
+
onBlur={() => setShow(false)}
|
|
480
|
+
>
|
|
481
|
+
{children}
|
|
482
|
+
</button>
|
|
483
|
+
{show && (
|
|
484
|
+
<div id={id} role="tooltip">
|
|
485
|
+
{text}
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
488
|
+
</>
|
|
489
|
+
);
|
|
490
|
+
};
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Critical Mistakes - Never Generate
|
|
494
|
+
|
|
495
|
+
```jsx
|
|
496
|
+
// ❌ FORBIDDEN: Redundant roles
|
|
497
|
+
<button role="button">Click</button>
|
|
498
|
+
<nav role="navigation">...</nav>
|
|
499
|
+
|
|
500
|
+
// ❌ FORBIDDEN: aria-label with visible text
|
|
501
|
+
<button aria-label="Submit">Submit</button>
|
|
502
|
+
|
|
503
|
+
// ❌ FORBIDDEN: Hiding interactive elements
|
|
504
|
+
<button aria-hidden="true">Click</button>
|
|
505
|
+
|
|
506
|
+
// ❌ FORBIDDEN: Role without keyboard support
|
|
507
|
+
<div role="button" onClick={handleClick}>Bad</div>
|
|
508
|
+
|
|
509
|
+
// ❌ FORBIDDEN: Positive tabIndex
|
|
510
|
+
<button tabIndex={1}>Bad</button>
|
|
511
|
+
|
|
512
|
+
// ❌ FORBIDDEN: Empty labels
|
|
513
|
+
<button aria-label="">Click</button>
|
|
514
|
+
<img alt="image" />
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## Always Generate These Instead
|
|
518
|
+
|
|
519
|
+
```jsx
|
|
520
|
+
// ✅ Native elements (no role needed)
|
|
521
|
+
<button>Click</button>
|
|
522
|
+
<nav>...</nav>
|
|
523
|
+
|
|
524
|
+
// ✅ No aria-label when text visible
|
|
525
|
+
<button>Submit</button>
|
|
526
|
+
|
|
527
|
+
// ✅ Hide decorative only
|
|
528
|
+
<button>
|
|
529
|
+
Save <SaveIcon aria-hidden="true" />
|
|
530
|
+
</button>
|
|
531
|
+
|
|
532
|
+
// ✅ Full keyboard support
|
|
533
|
+
<div
|
|
534
|
+
role="button"
|
|
535
|
+
tabIndex={0}
|
|
536
|
+
onClick={handleClick}
|
|
537
|
+
onKeyDown={(e) => {
|
|
538
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
handleClick();
|
|
541
|
+
}
|
|
542
|
+
}}
|
|
543
|
+
>
|
|
544
|
+
Good
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
// ✅ Natural tab order
|
|
548
|
+
<button>First</button>
|
|
549
|
+
<button>Second</button>
|
|
550
|
+
|
|
551
|
+
// ✅ Descriptive labels
|
|
552
|
+
<button aria-label="Close menu">×</button>
|
|
553
|
+
<img alt="Company logo" />
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Validation Checklist
|
|
557
|
+
|
|
558
|
+
Before finalizing code, verify:
|
|
559
|
+
|
|
560
|
+
- [ ] Native HTML used when possible
|
|
561
|
+
- [ ] No redundant roles on native elements
|
|
562
|
+
- [ ] All IDs referenced by ARIA exist and are unique
|
|
563
|
+
- [ ] `aria-hidden` never on interactive elements
|
|
564
|
+
- [ ] Boolean ARIA uses `true`/`false`, not strings
|
|
565
|
+
- [ ] All interactive elements keyboard accessible
|
|
566
|
+
- [ ] No positive tabIndex values
|
|
567
|
+
- [ ] Dynamic content has live regions
|
|
568
|
+
- [ ] Form fields have labels
|