@minhduydev/mdpi 0.4.0 → 0.4.1
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/dist/index.js +1 -1
- package/dist/template/.pi/VERSION +1 -1
- package/dist/template/.pi/extensions/templates-injector.ts +1 -1
- package/dist/template/.pi/skills/INDEX.md +43 -12
- package/dist/template/.pi/skills/accessibility-audit/SKILL.md +8 -2
- package/dist/template/.pi/skills/baseline-ui/SKILL.md +211 -0
- package/dist/template/.pi/skills/design-taste-frontend/SKILL.md +53 -42
- package/dist/template/.pi/skills/fixing-accessibility/SKILL.md +509 -0
- package/dist/template/.pi/skills/frontend-design/SKILL.md +59 -46
- package/dist/template/.pi/skills/frontend-ui-engineering/SKILL.md +21 -27
- package/dist/template/.pi/skills/oklch-color-workflow/SKILL.md +426 -0
- package/dist/template/.pi/skills/production-hardening/SKILL.md +652 -0
- package/dist/template/.pi/skills/ui-craft-principles/SKILL.md +564 -0
- package/dist/template/.pi/skills/ui-quality-audit/SKILL.md +329 -0
- package/dist/template/.pi/templates/DESIGN.md +76 -0
- package/dist/template/.pi/workflows/INDEX.md +2 -1
- package/dist/template/.pi/workflows/frontend-feature-workflow.md +343 -0
- package/dist/template/.pi/workflows/quality-loop.md +1 -1
- package/package.json +1 -1
- /package/dist/template/.pi/templates/{design.md → feature-design.md} +0 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fixing-accessibility
|
|
3
|
+
description: Actionable WCAG 2.1 AA accessibility fixes — not just audit, but concrete code fixes with before/after examples
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fixing Accessibility
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
- After building UI components — run this as a quality gate before merging
|
|
11
|
+
- When fixing accessibility issues found by axe-core, Lighthouse, or manual testing
|
|
12
|
+
- When adding keyboard navigation, ARIA labels, focus management, or screen reader support
|
|
13
|
+
- When retrofitting accessibility onto existing components
|
|
14
|
+
- During code review of UI changes — check for common accessibility regressions
|
|
15
|
+
|
|
16
|
+
## When NOT to Use
|
|
17
|
+
|
|
18
|
+
- For accessibility audits without implementation (use `ui-quality-audit` instead)
|
|
19
|
+
- When building non-interactive content (static markup with no dynamic behavior)
|
|
20
|
+
- When the only user of the application is yourself and you don't need assistive tech
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Priority Categories
|
|
25
|
+
|
|
26
|
+
### 1. Keyboard Navigation
|
|
27
|
+
|
|
28
|
+
**Why it matters:** ~25% of web users rely on keyboard navigation. If they can't tab through your interface, it's unusable.
|
|
29
|
+
|
|
30
|
+
**Common issues:**
|
|
31
|
+
- Interactive elements aren't focusable
|
|
32
|
+
- Custom components (select, dropdown, menu) trap or skip focus
|
|
33
|
+
- Tab order doesn't match visual order
|
|
34
|
+
- No visible focus indicator
|
|
35
|
+
|
|
36
|
+
**Fixes:**
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// BEFORE — custom dropdown not keyboard-accessible
|
|
40
|
+
<div className="relative">
|
|
41
|
+
<div onClick={() => setOpen(!open)}>Select option</div>
|
|
42
|
+
{open && items.map(item => (
|
|
43
|
+
<div key={item} onClick={() => select(item)}>{item}</div>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
// AFTER — proper button + listbox pattern
|
|
48
|
+
<div className="relative">
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => setOpen(!open)}
|
|
52
|
+
aria-expanded={open}
|
|
53
|
+
aria-haspopup="listbox"
|
|
54
|
+
className="..."
|
|
55
|
+
>
|
|
56
|
+
{selected || 'Select option'}
|
|
57
|
+
</button>
|
|
58
|
+
{open && (
|
|
59
|
+
<ul role="listbox" className="absolute ...">
|
|
60
|
+
{items.map(item => (
|
|
61
|
+
<li
|
|
62
|
+
key={item}
|
|
63
|
+
role="option"
|
|
64
|
+
tabIndex={-1}
|
|
65
|
+
onClick={() => select(item)}
|
|
66
|
+
onKeyDown={(e) => { if (e.key === 'Enter') select(item); }}
|
|
67
|
+
aria-selected={item === selected}
|
|
68
|
+
className="..."
|
|
69
|
+
>
|
|
70
|
+
{item}
|
|
71
|
+
</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// BEFORE — missing focus indicator on interactive card
|
|
80
|
+
<div onClick={() => navigate(id)} className="rounded-lg p-4 cursor-pointer">
|
|
81
|
+
<h3>{title}</h3>
|
|
82
|
+
<p>{desc}</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
// AFTER — focusable with visible ring
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => navigate(id)}
|
|
89
|
+
className="rounded-lg p-4 text-left w-full focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
|
90
|
+
>
|
|
91
|
+
<h3>{title}</h3>
|
|
92
|
+
<p className="text-muted-foreground">{desc}</p>
|
|
93
|
+
</button>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Quick check:** Tab through the entire page. Every interactive element should receive focus in a logical order. You should never get stuck in a focus trap.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### 2. Focus Management
|
|
101
|
+
|
|
102
|
+
**Why it matters:** Users must know where they are at all times. Lost focus = lost user.
|
|
103
|
+
|
|
104
|
+
**Common issues:**
|
|
105
|
+
- Focus doesn't move to newly opened modal/dialog
|
|
106
|
+
- Focus gets reset to top of page after dynamic content change
|
|
107
|
+
- Focus outline is removed via `outline: none` without alternative
|
|
108
|
+
|
|
109
|
+
**Fixes:**
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// BEFORE — modal opens, focus stays on trigger button
|
|
113
|
+
function Modal({ open, onClose, children }) {
|
|
114
|
+
if (!open) return null;
|
|
115
|
+
return (
|
|
116
|
+
<div className="fixed inset-0 bg-black/50">
|
|
117
|
+
<div className="...">
|
|
118
|
+
{children}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// AFTER — auto-focus dialog and Escape to close (for full focus-trap, see `frontend-design` interaction patterns)
|
|
125
|
+
function Modal({ open, onClose, children }) {
|
|
126
|
+
const dialogRef = useRef(null);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (open) {
|
|
130
|
+
// Focus the dialog container
|
|
131
|
+
dialogRef.current?.focus();
|
|
132
|
+
}
|
|
133
|
+
}, [open]);
|
|
134
|
+
|
|
135
|
+
// Handle Escape key
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!open) return;
|
|
138
|
+
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
|
139
|
+
document.addEventListener('keydown', handler);
|
|
140
|
+
return () => document.removeEventListener('keydown', handler);
|
|
141
|
+
}, [open, onClose]);
|
|
142
|
+
|
|
143
|
+
if (!open) return null;
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
role="dialog"
|
|
147
|
+
aria-modal="true"
|
|
148
|
+
ref={dialogRef}
|
|
149
|
+
tabIndex={-1}
|
|
150
|
+
className="fixed inset-0 bg-black/50 flex items-center justify-center"
|
|
151
|
+
onKeyDown={(e) => { if (e.key === 'Escape') onClose(); }}
|
|
152
|
+
>
|
|
153
|
+
<div className="bg-white rounded-lg p-6">
|
|
154
|
+
{children}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
/* BEFORE — removes focus outline entirely (DO NOT DO THIS) */
|
|
163
|
+
*:focus {
|
|
164
|
+
outline: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* AFTER — custom focus ring that's visible */
|
|
168
|
+
*:focus-visible {
|
|
169
|
+
outline: 2px solid var(--color-primary);
|
|
170
|
+
outline-offset: 2px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Remove outline only for mouse clicks, keep keyboard focus visible */
|
|
174
|
+
*:focus:not(:focus-visible) {
|
|
175
|
+
outline: none;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### 3. ARIA Labels
|
|
182
|
+
|
|
183
|
+
**Why it matters:** ARIA labels provide screen reader context that visual users take for granted.
|
|
184
|
+
|
|
185
|
+
**Common issues:**
|
|
186
|
+
- Icon-only buttons without labels
|
|
187
|
+
- Dynamic content changes not announced
|
|
188
|
+
- Incorrect or redundant ARIA (overriding semantic HTML)
|
|
189
|
+
|
|
190
|
+
**Fixes:**
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// BEFORE — icon button with no label
|
|
194
|
+
<button onClick={onDelete}>
|
|
195
|
+
<TrashIcon className="h-5 w-5" />
|
|
196
|
+
</button>
|
|
197
|
+
|
|
198
|
+
// AFTER — labeled for screen readers
|
|
199
|
+
<button onClick={onDelete} aria-label="Delete item">
|
|
200
|
+
<TrashIcon className="h-5 w-5" aria-hidden="true" />
|
|
201
|
+
</button>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
// BEFORE — dynamic content without announcement
|
|
206
|
+
<div>
|
|
207
|
+
{items.length === 0 && <p>No results found</p>}
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
// AFTER — announces content changes
|
|
211
|
+
<div aria-live="polite" aria-atomic="true">
|
|
212
|
+
{items.length === 0 && <p>No results found</p>}
|
|
213
|
+
</div>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Rule of thumb:** If it has no visible text label, it needs `aria-label`. If it has a visible label, use `aria-labelledby` pointing to the label's `id`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### 4. Color Contrast
|
|
221
|
+
|
|
222
|
+
**Why it matters:** WCAG 2.1 AA requires 4.5:1 for normal text, 3:1 for large text (18px+ bold or 24px+ regular).
|
|
223
|
+
|
|
224
|
+
**Common issues:**
|
|
225
|
+
- Gray text on white backgrounds (e.g., `text-gray-400` on white)
|
|
226
|
+
- Low-contrast placeholder text
|
|
227
|
+
- Links that only differ by color
|
|
228
|
+
- Disabled buttons with insufficient contrast
|
|
229
|
+
|
|
230
|
+
**Fixes:**
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
// BEFORE — insufficient contrast
|
|
234
|
+
<p className="text-gray-400 text-sm">Supporting text</p>
|
|
235
|
+
|
|
236
|
+
// AFTER — meets 4.5:1
|
|
237
|
+
<p className="text-gray-600 text-sm">Supporting text</p>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
// BEFORE — link only distinguishable by color
|
|
242
|
+
<span className="text-gray-600">
|
|
243
|
+
Terms of <a href="/service" className="text-blue-500">Service</a>
|
|
244
|
+
</span>
|
|
245
|
+
|
|
246
|
+
// AFTER — link has underline (non-color cue)
|
|
247
|
+
<span className="text-gray-600">
|
|
248
|
+
Terms of <a href="/service" className="text-blue-500 underline">Service</a>
|
|
249
|
+
</span>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Quick check:** Use the browser DevTools color picker — it shows contrast ratio. Check body text first, then small text, then placeholder text.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### 5. Heading Hierarchy
|
|
257
|
+
|
|
258
|
+
**Why it matters:** Screen reader users navigate pages by heading structure. Bad hierarchy means they can't understand the page layout.
|
|
259
|
+
|
|
260
|
+
**Common issues:**
|
|
261
|
+
- Skipping levels (h1 → h3)
|
|
262
|
+
- Multiple h1s on one page
|
|
263
|
+
- Headings selected by visual size, not semantic level
|
|
264
|
+
- No h1 on the page
|
|
265
|
+
|
|
266
|
+
**Fixes:**
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
// BEFORE — skipped level, no h1
|
|
270
|
+
<div className="text-3xl font-bold">Product Page</div>
|
|
271
|
+
<h3 className="text-xl">Reviews</h3>
|
|
272
|
+
<h4 className="text-lg">User Review</h4>
|
|
273
|
+
|
|
274
|
+
// AFTER — proper hierarchy
|
|
275
|
+
<h1 className="text-3xl font-bold">Product Page</h1>
|
|
276
|
+
<h2 className="text-xl">Reviews</h2>
|
|
277
|
+
<h3 className="text-lg">User Review</h3>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Quick check:** Run the WAVE browser extension or use your browser's accessibility panel to view heading structure. It should read like a table of contents: one h1, logical nesting.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### 6. Form Labels
|
|
285
|
+
|
|
286
|
+
**Why it matters:** Every form input needs an associated label for screen readers and click target expansion.
|
|
287
|
+
|
|
288
|
+
**Common issues:**
|
|
289
|
+
- Placeholder as label (disappears on input)
|
|
290
|
+
- Missing `for`/`id` association
|
|
291
|
+
- Error messages not associated with inputs
|
|
292
|
+
- Required fields not indicated programmatically
|
|
293
|
+
|
|
294
|
+
**Fixes:**
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
// BEFORE — placeholder-only label
|
|
298
|
+
<input
|
|
299
|
+
type="email"
|
|
300
|
+
placeholder="Email address"
|
|
301
|
+
className="..."
|
|
302
|
+
/>
|
|
303
|
+
|
|
304
|
+
// AFTER — proper label association
|
|
305
|
+
<label htmlFor="email" className="block text-sm font-medium">
|
|
306
|
+
Email address
|
|
307
|
+
</label>
|
|
308
|
+
<input
|
|
309
|
+
id="email"
|
|
310
|
+
type="email"
|
|
311
|
+
placeholder="you@example.com"
|
|
312
|
+
aria-required="true"
|
|
313
|
+
className="mt-1 ..."
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
// BEFORE — error message not associated
|
|
319
|
+
<div>
|
|
320
|
+
<label htmlFor="name">Name</label>
|
|
321
|
+
<input id="name" />
|
|
322
|
+
<p className="text-red-500">Name is required</p>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
// AFTER — error message linked via aria-describedby
|
|
326
|
+
<div>
|
|
327
|
+
<label htmlFor="name">Name</label>
|
|
328
|
+
<input
|
|
329
|
+
id="name"
|
|
330
|
+
aria-invalid={!!error}
|
|
331
|
+
aria-describedby={error ? 'name-error' : undefined}
|
|
332
|
+
/>
|
|
333
|
+
{error && (
|
|
334
|
+
<p id="name-error" className="text-red-500 text-sm" role="alert">
|
|
335
|
+
{error}
|
|
336
|
+
</p>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### 7. Image Alt Text
|
|
344
|
+
|
|
345
|
+
**Why it matters:** Without alt text, screen readers read the image filename or say "image" — zero information.
|
|
346
|
+
|
|
347
|
+
**Common issues:**
|
|
348
|
+
- Missing `alt` on informative images
|
|
349
|
+
- Redundant `alt=""` on images that should be described
|
|
350
|
+
- Alt text that duplicates adjacent text
|
|
351
|
+
- Decorative images missing `alt=""`
|
|
352
|
+
|
|
353
|
+
**Fixes:**
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
// BEFORE — informative image without alt
|
|
357
|
+
<img src="/team/maria.jpg" className="rounded-full" />
|
|
358
|
+
|
|
359
|
+
// AFTER — describes the image content
|
|
360
|
+
<img src="/team/maria.jpg" alt="Maria Chen, Head of Design" className="rounded-full" />
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
// BEFORE — decorative icon without alt="" (screen reader reads filename)
|
|
365
|
+
<img src="/decorative-divider.svg" className="h-4 w-full" />
|
|
366
|
+
|
|
367
|
+
// AFTER — explicitly decorative
|
|
368
|
+
<img src="/decorative-divider.svg" alt="" role="presentation" className="h-4 w-full" />
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Rule of thumb:**
|
|
372
|
+
- Informative → describe the content/function
|
|
373
|
+
- Decorative → `alt=""` (empty string)
|
|
374
|
+
- Link → describe the link destination
|
|
375
|
+
- Complex (chart/diagram) → link to a text description nearby
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### 8. Touch Targets
|
|
380
|
+
|
|
381
|
+
**Why it matters:** WCAG 2.1 requires touch targets ≥ 44×44px. Small targets frustrate all users, especially on mobile.
|
|
382
|
+
|
|
383
|
+
**Common issues:**
|
|
384
|
+
- Small icon buttons (24×24 or smaller)
|
|
385
|
+
- Closely packed links in nav or footer
|
|
386
|
+
- Small form inputs on mobile
|
|
387
|
+
|
|
388
|
+
**Fixes:**
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
// BEFORE — 28×28 icon button, hard to tap
|
|
392
|
+
<button className="p-1">
|
|
393
|
+
<XIcon className="h-5 w-5" />
|
|
394
|
+
</button>
|
|
395
|
+
|
|
396
|
+
// AFTER — padded to 44×44 minimum
|
|
397
|
+
<button
|
|
398
|
+
className="p-2.5"
|
|
399
|
+
style={{ minWidth: '44px', minHeight: '44px' }}
|
|
400
|
+
aria-label="Close"
|
|
401
|
+
>
|
|
402
|
+
<XIcon className="h-5 w-5" aria-hidden="true" />
|
|
403
|
+
</button>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
```css
|
|
407
|
+
/* BEFORE — nav links with no spacing */
|
|
408
|
+
.nav-link { display: inline; }
|
|
409
|
+
|
|
410
|
+
/* AFTER — each link has 44px minimum hit area */
|
|
411
|
+
.nav-link {
|
|
412
|
+
display: inline-flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
min-height: 44px;
|
|
415
|
+
padding: 8px 12px;
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Quick check:** On mobile viewport (375px), try tapping each interactive element. If you miss or hit the wrong thing, the target is too small or too close.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
### 9. Screen Reader Content
|
|
424
|
+
|
|
425
|
+
**Why it matters:** Screen readers linearize content. Off-screen content, live regions, and status messages must be handled explicitly.
|
|
426
|
+
|
|
427
|
+
**Common issues:**
|
|
428
|
+
- Loading states not announced
|
|
429
|
+
- Sort/filter changes not announced
|
|
430
|
+
- Off-screen/navigable content not hidden from screen readers
|
|
431
|
+
- Status messages without `role="status"` or `aria-live`
|
|
432
|
+
|
|
433
|
+
**Fixes:**
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
// BEFORE — loading not announced
|
|
437
|
+
{loading && <Spinner />}
|
|
438
|
+
{!loading && <DataGrid data={items} />}
|
|
439
|
+
|
|
440
|
+
// AFTER — loading announced via aria-live
|
|
441
|
+
<div aria-live="polite" aria-atomic="true">
|
|
442
|
+
{loading && (
|
|
443
|
+
<div role="status">
|
|
444
|
+
<span className="sr-only">Loading data...</span>
|
|
445
|
+
<Spinner aria-hidden="true" />
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
{!loading && <DataGrid data={items} />}
|
|
449
|
+
</div>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
```tsx
|
|
453
|
+
// BEFORE — sort change not announced
|
|
454
|
+
<button onClick={() => setSort('date')}>Sort by date</button>
|
|
455
|
+
|
|
456
|
+
// AFTER — sort change announced
|
|
457
|
+
<button onClick={() => { setSort('date'); setAnnounce(`Sorted by date`); }}>
|
|
458
|
+
Sort by date
|
|
459
|
+
</button>
|
|
460
|
+
<div aria-live="polite" aria-atomic="true" className="sr-only">
|
|
461
|
+
{announcement}
|
|
462
|
+
</div>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Tool Boundaries
|
|
468
|
+
|
|
469
|
+
| Tool | Can detect | Cannot detect |
|
|
470
|
+
|------|-----------|---------------|
|
|
471
|
+
| **axe-core / Lighthouse** | Missing alt text, color contrast, heading gaps, missing labels, ARIA errors | Focus order correctness, screen reader flow, real-world keyboard usability |
|
|
472
|
+
| **Manual keyboard test** | Tab order, focus traps, focus visibility | ARIA correctness, label quality, contrast ratios |
|
|
473
|
+
| **Screen reader (VoiceOver/NVDA)** | Announcement quality, label clarity, live region behavior | Color contrast (programmatic), specific WCAG violations |
|
|
474
|
+
| **Color contrast analyzer** | Exact contrast ratios, WCAG pass/fail | Functional usability, real-world readability |
|
|
475
|
+
|
|
476
|
+
**Recommended workflow:**
|
|
477
|
+
|
|
478
|
+
1. Run `axe-core` / Lighthouse — fix all detected issues
|
|
479
|
+
2. Tab through the page — fix focus order and visibility
|
|
480
|
+
3. Test with screen reader on — fix announcement quality
|
|
481
|
+
4. Check contrast on all text/background pairs — fix failures
|
|
482
|
+
5. Zoom to 200% / 400% — fix content reflow issues
|
|
483
|
+
|
|
484
|
+
## Don't
|
|
485
|
+
|
|
486
|
+
| Pattern | Replacement | Because |
|
|
487
|
+
|---------|-------------|---------|
|
|
488
|
+
| Custom interactive elements not keyboard-accessible | Use native `<button>`, `<a>`, or implement proper ARIA + keyboard handlers | ~25% of users rely on keyboard navigation |
|
|
489
|
+
| Removing focus outlines with `outline: none` | Use `:focus-visible` with a visible focus ring | Lost focus indicator makes site unusable for keyboard users |
|
|
490
|
+
| Icon-only buttons without `aria-label` | Add `aria-label` describing the action | Screen readers cannot convey the action |
|
|
491
|
+
| Placeholder as only form label | Associate `<label>` with `htmlFor`/`id` | Placeholder disappears on input, breaking assistive tech |
|
|
492
|
+
| Missing `alt` text on informative images | Describe content or function in `alt` attribute | Screen reader reads filename instead |
|
|
493
|
+
| Touch targets under 44×44px | Pad interactive elements to 44px minimum hit area | WCAG 2.1 requires 44px for touch targets |
|
|
494
|
+
| Skipping heading levels (h1 → h3) | Maintain sequential hierarchy (h1 → h2 → h3) | Screen reader navigation relies on heading structure |
|
|
495
|
+
|
|
496
|
+
## Verification
|
|
497
|
+
|
|
498
|
+
- [ ] Tab through every interactive element — logical order, visible focus, no traps
|
|
499
|
+
- [ ] All icon-only buttons have `aria-label`
|
|
500
|
+
- [ ] All form inputs have associated `<label>` elements
|
|
501
|
+
- [ ] All images have appropriate `alt` text (informative or decorative `alt=""`)
|
|
502
|
+
- [ ] No `outline: none` without `:focus-visible` fallback
|
|
503
|
+
- [ ] Color contrast ≥ 4.5:1 for normal text, ≥ 3:1 for large text
|
|
504
|
+
- [ ] Heading hierarchy is logical (single h1, no skipped levels)
|
|
505
|
+
- [ ] Touch targets ≥ 44×44px on all interactive elements
|
|
506
|
+
- [ ] Dynamic content changes are announced via `aria-live` regions
|
|
507
|
+
- [ ] Modals/dialogs trap focus and return focus on close
|
|
508
|
+
- [ ] Error messages associated with inputs via `aria-describedby`
|
|
509
|
+
- [ ] Page works zoomed to 200% without horizontal scroll or content loss
|
|
@@ -3,6 +3,8 @@ name: frontend-design
|
|
|
3
3
|
description: MUST load when building any web UI with React-based frameworks — components, pages, or full applications. Covers Tailwind CSS v4, shadcn/ui, Motion animations. Base UI implementation skill; combine with aesthetic overlays (minimalist-ui, high-end-visual-design) for specific styles.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
**Aesthetic Context:** Your implementation must reflect the project `.pi/DESIGN.md` identity. Before writing any component, internalize the Overview & Mood, Colors, and Typography sections. All code output should feel like it belongs to the same intentional design system.
|
|
7
|
+
|
|
6
8
|
# Frontend Design
|
|
7
9
|
|
|
8
10
|
## When to Use
|
|
@@ -18,6 +20,17 @@ description: MUST load when building any web UI with React-based frameworks —
|
|
|
18
20
|
|
|
19
21
|
- Backend-only tasks or minimal UI with no visual design requirements.
|
|
20
22
|
|
|
23
|
+
## Relationship to Other Skills
|
|
24
|
+
|
|
25
|
+
| Skill | Role |
|
|
26
|
+
|-------|------|
|
|
27
|
+
| `design-taste-frontend` | Upstream — sets aesthetic baseline and anti-AI-slops. Load BEFORE this skill. |
|
|
28
|
+
| `frontend-ui-engineering` | Sibling — handles component implementation, accessibility, and state patterns. |
|
|
29
|
+
| `react-best-practices` | Complement — React/Next.js performance patterns. |
|
|
30
|
+
| `baseline-ui` | Quick deslop pass for automatic fixes (spacing, typography). |
|
|
31
|
+
|
|
32
|
+
**Pipeline:** `design-taste-frontend` → `frontend-design` → `frontend-ui-engineering`
|
|
33
|
+
|
|
21
34
|
## Reference Documentation
|
|
22
35
|
|
|
23
36
|
### Tailwind CSS v4.1
|
|
@@ -67,7 +80,7 @@ For sophisticated compositions: posters, brand materials, design systems.
|
|
|
67
80
|
- `./references/design/interaction.md` - State models, focus, dialogs/popovers, loading patterns
|
|
68
81
|
- `./references/design/ux-writing.md` - Button copy, error structure, empty states, i18n
|
|
69
82
|
|
|
70
|
-
Search: `
|
|
83
|
+
Search: `tinted neutrals`, `focus-visible`, `verb + object`, `65ch`
|
|
71
84
|
|
|
72
85
|
## Design Thinking
|
|
73
86
|
|
|
@@ -79,49 +92,56 @@ Before coding, commit to BOLD aesthetic direction:
|
|
|
79
92
|
|
|
80
93
|
Bold maximalism and refined minimalism both work. Key is intentionality.
|
|
81
94
|
|
|
82
|
-
##
|
|
83
|
-
|
|
84
|
-
These patterns immediately signal "AI made this." Avoid them all.
|
|
95
|
+
## Don't
|
|
85
96
|
|
|
86
97
|
### Typography
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
| Pattern | Replacement | Because |
|
|
100
|
+
|---------|-------------|---------|
|
|
101
|
+
| Inter, Roboto, Arial as display fonts | Distinctive display fonts (Instrument Sans, Outfit, Fraunces) | Overused fonts signal generic design |
|
|
102
|
+
| Monospace used as "developer aesthetic" shorthand | Purposeful type choice; mono only for code/data | Mono-as-aesthetic reads as placeholder design |
|
|
103
|
+
| Big icons centered above every heading | Integrated icon + heading lockup, or icon inline | Giant centered icons feel template-generated |
|
|
104
|
+
| Using `px` for body text | `rem`/`em` to respect user font-size preferences | `px` ignores accessibility and user settings |
|
|
92
105
|
|
|
93
106
|
### Color
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
| Pattern | Replacement | Because |
|
|
109
|
+
|---------|-------------|---------|
|
|
110
|
+
| Gray text on colored backgrounds | Darker shade of the background color | Gray-on-color fails contrast and looks muddy |
|
|
111
|
+
| Pure `#000` or `#fff` | Tinted near-black or near-white | Pure black/white don't exist in natural light |
|
|
112
|
+
| Gradient text on headings or metrics | Solid, well-chosen heading color | Gradient text is a design crutch |
|
|
113
|
+
| `rgba()` / heavy alpha transparency as primary palette | Explicit, named color values | Heavy alpha stacking creates unpredictable colors |
|
|
100
114
|
|
|
101
115
|
### Layout
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
| Pattern | Replacement | Because |
|
|
118
|
+
|---------|-------------|---------|
|
|
119
|
+
| Cards nested inside cards | Typography, spacing, dividers for hierarchy | Nested cards create visual noise without purpose |
|
|
120
|
+
| Identical card grids (icon + heading + text ×3-6) | Varied layout with purposeful asymmetry | Repeated identical cards is the #1 AI tell |
|
|
121
|
+
| Hero metric template (big number + small label + gradient accent) | Contextual data display — number embedded in prose or card | Generic hero metrics are the startup-template cliché |
|
|
122
|
+
| Center-aligning everything | Left-align content blocks; reserve center for short hero headlines | Center-aligned body text is hard to scan |
|
|
123
|
+
| Same spacing everywhere (no visual rhythm) | Use proximity to group related items; vary spacing to create sections | Uniform spacing flattens hierarchy |
|
|
108
124
|
|
|
109
125
|
### Visual
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
| Pattern | Replacement | Because |
|
|
128
|
+
|---------|-------------|---------|
|
|
129
|
+
| Glassmorphism used decoratively | Flat surfaces or layered shadows | Glassmorphism needs a functional reason for depth |
|
|
130
|
+
| Thick colored border on one side of rounded rectangles | Subtle border or shadow on entire element | One-sided colored borders are a dated pattern |
|
|
131
|
+
| Sparklines as decoration (not connected to real data) | Real sparklines from actual data, or omit entirely | Decorative sparklines are fake data theater |
|
|
132
|
+
| Generic drop shadows on everything | Intentional shadow hierarchy — only where depth communicates meaning | Shadow-everywhere flattens the depth language |
|
|
133
|
+
| Rounded rectangles as the only shape language | Mix shapes: sharp corners, soft corners, circles, organic shapes | Single-shape designs feel templated |
|
|
116
134
|
|
|
117
135
|
### Motion
|
|
118
136
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
|
|
137
|
+
| Pattern | Replacement | Because |
|
|
138
|
+
|---------|-------------|---------|
|
|
139
|
+
| Bounce or elastic easing on UI | Exponential easing `cubic-bezier(0.16, 1, 0.3, 1)` | Real objects decelerate smoothly, not bounce |
|
|
140
|
+
| Animating `height`, `width`, `padding`, `margin` | Animate only `transform` and `opacity` | Layout animations cause expensive repaints |
|
|
141
|
+
| Default `ease` | Exponential easing curves tuned to animation purpose | Default `ease` is a compromise rarely optimal |
|
|
142
|
+
| Missing `prefers-reduced-motion` handling | Always respect reduced motion preferences | ~35% of adults over 40 prefer reduced motion |
|
|
123
143
|
|
|
124
|
-
> **The
|
|
144
|
+
> **The Slop Test:** If you showed this interface to someone and said "AI made this," would they believe you immediately? If yes, that's the problem.
|
|
125
145
|
|
|
126
146
|
## Best Practices
|
|
127
147
|
|
|
@@ -235,28 +255,21 @@ Create atmosphere: gradient meshes, noise textures, geometric patterns, layered
|
|
|
235
255
|
|
|
236
256
|
| Rationalization | Reality |
|
|
237
257
|
|---|---|
|
|
238
|
-
| "The
|
|
258
|
+
| "The default look is fine for now" | Default styles signal low quality. Use the design system from the start. |
|
|
239
259
|
| "I'll make it responsive later" | Retrofitting responsive design is 3x harder than building it mobile-first. |
|
|
240
260
|
| "Accessibility is a nice-to-have" | It's a legal requirement in many jurisdictions and an engineering quality standard. |
|
|
241
261
|
| "This is just a prototype" | Prototypes become production code. Build the foundation right from the start. |
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
- Components exceeding 200 lines (split them)
|
|
246
|
-
- Inline styles or arbitrary pixel values
|
|
247
|
-
- Missing error states, loading states, or empty states
|
|
248
|
-
- No keyboard navigation testing
|
|
249
|
-
- Color as the sole indicator of state (red/green without text or icons)
|
|
250
|
-
- Generic "AI look" (purple gradients, oversized cards, stock layouts)
|
|
262
|
+
| "Typography doesn't matter for functionality" | Typography is 95% of web design. Bad type ruins even good layouts. |
|
|
263
|
+
| "Users won't notice the details" | Users may not articulate it, but they feel quality. Details accumulate into perception. |
|
|
251
264
|
|
|
252
265
|
## Verification
|
|
253
266
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
- [ ]
|
|
257
|
-
- [ ]
|
|
258
|
-
- [ ]
|
|
267
|
+
- [ ] Design tokens defined: primitives + semantic layer, with dark mode variants
|
|
268
|
+
- [ ] Typography: distinctive font pairing, fluid sizing with `clamp()`, `max-width: 65ch` on body text
|
|
269
|
+
- [ ] Color: OKLCH tokens, tinted neutrals (chroma 0.01-0.02), sufficient contrast (4.5:1 min)
|
|
270
|
+
- [ ] Motion: exponential easing only, `prefers-reduced-motion` handled, only `transform` + `opacity` animated
|
|
271
|
+
- [ ] Spacing: consistent 4pt scale, `gap` over margins, self-adjusting grids
|
|
259
272
|
- [ ] Responsive: works at 320px, 768px, 1024px, 1440px
|
|
260
|
-
- [ ]
|
|
261
|
-
- [ ]
|
|
262
|
-
- [ ] No
|
|
273
|
+
- [ ] Interaction: all 8 states designed, `:focus-visible` used, skeletons over spinners
|
|
274
|
+
- [ ] UX Writing: verb + object buttons, error = what + why + fix, empty states are onboarding
|
|
275
|
+
- [ ] No banned fonts, no gray-on-color text, no pure black/white
|