@minhduydev/mdpi 0.4.0 → 0.5.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/dist/index.js +1 -1
- package/dist/template/.pi/VERSION +1 -1
- package/dist/template/.pi/extensions/templates-injector.ts +34 -6
- package/dist/template/.pi/prompts/INDEX.md +3 -9
- package/dist/template/.pi/skills/INDEX.md +81 -19
- 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/dcp-hygiene/SKILL.md +1 -1
- 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 +60 -47
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-advanced.md +88 -15
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-core.md +148 -13
- package/dist/template/.pi/skills/frontend-design/references/shadcn/setup.md +127 -20
- package/dist/template/.pi/skills/frontend-ui-engineering/SKILL.md +21 -27
- package/dist/template/.pi/skills/nextjs-app-router/SKILL.md +334 -0
- package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -0
- 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/react-best-practices/SKILL.md +79 -1
- package/dist/template/.pi/skills/react-compiler/SKILL.md +237 -0
- package/dist/template/.pi/skills/react-hook-form/SKILL.md +374 -0
- package/dist/template/.pi/skills/react-server-actions/SKILL.md +299 -0
- package/dist/template/.pi/skills/shadcn-ui/SKILL.md +404 -0
- package/dist/template/.pi/skills/tanstack-query/SKILL.md +330 -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/skills/v0/SKILL.md +264 -0
- package/dist/template/.pi/skills/zustand/SKILL.md +333 -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/prompts/loop-check.md +0 -87
- package/dist/template/.pi/prompts/loop-init.md +0 -157
- package/dist/template/.pi/prompts/loop-review.md +0 -90
- package/dist/template/.pi/skills/loop-audit/SKILL.md +0 -141
- package/dist/template/.pi/skills/loop-cost/SKILL.md +0 -130
- package/dist/template/.pi/skills/loop-engineering/SKILL.md +0 -175
- package/dist/template/.pi/templates/loop-github-action.yml +0 -162
- package/dist/template/.pi/templates/loop-orchestrator.sh +0 -514
- package/dist/template/.pi/templates/loop-orchestrator.test.ts +0 -332
- package/dist/template/.pi/templates/loop-orchestrator.ts +0 -936
- package/dist/template/.pi/templates/loop-state.json +0 -24
- package/dist/template/.pi/templates/loop-state.md +0 -98
- package/dist/template/.pi/templates/loop-vision.md +0 -110
- /package/dist/template/.pi/templates/{design.md → feature-design.md} +0 -0
|
@@ -3,6 +3,8 @@ name: design-taste-frontend
|
|
|
3
3
|
description: Use when building any web UI as the BASE aesthetic layer to override default LLM design biases. Enforces strict typography, color, spacing, and component architecture rules. Load BEFORE frontend-design when premium visual quality is required.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
**Aesthetic Context:** This is a premium aesthetic layer. Read `.pi/DESIGN.md` first to internalize the project's visual identity — every typographic, color, and spacing rule here is shaped by that mood. Design taste means the design feels intentional, not AI-generated.
|
|
7
|
+
|
|
6
8
|
## When to Use
|
|
7
9
|
|
|
8
10
|
- When building any web UI that needs to override default LLM design biases
|
|
@@ -15,7 +17,7 @@ description: Use when building any web UI as the BASE aesthetic layer to overrid
|
|
|
15
17
|
- For non-UI tasks (backend, CLI, data processing)
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
#
|
|
20
|
+
# Design Taste Frontend
|
|
19
21
|
|
|
20
22
|
## 1. ACTIVE BASELINE CONFIGURATION
|
|
21
23
|
* DESIGN_VARIANCE: 8 (1=Perfect Symmetry, 10=Artsy Chaos)
|
|
@@ -43,7 +45,6 @@ Unless the user explicitly specifies a different stack, adhere to these structur
|
|
|
43
45
|
* **Grid over Flex-Math:** NEVER use complex flexbox percentage math (`w-[calc(33%-1rem)]`). ALWAYS use CSS Grid (`grid grid-cols-1 md:grid-cols-3 gap-6`) for reliable structures.
|
|
44
46
|
* **Icons:** You MUST use exactly `@phosphor-icons/react` or `@radix-ui/react-icons` as the import paths (check installed version). Standardize `strokeWidth` globally (e.g., exclusively use `1.5` or `2.0`).
|
|
45
47
|
|
|
46
|
-
|
|
47
48
|
## 3. DESIGN ENGINEERING DIRECTIVES (Bias Correction)
|
|
48
49
|
LLMs have statistical biases toward specific UI cliché patterns. Proactively construct premium interfaces using these engineered rules:
|
|
49
50
|
|
|
@@ -55,7 +56,7 @@ LLMs have statistical biases toward specific UI cliché patterns. Proactively co
|
|
|
55
56
|
|
|
56
57
|
**Rule 2: Color Calibration**
|
|
57
58
|
* **Constraint:** Max 1 Accent Color. Saturation < 80%.
|
|
58
|
-
* **THE LILA BAN:** The "AI Purple/Blue" aesthetic is strictly BANNED. No purple button glows, no neon gradients. Use absolute neutral bases (Zinc/Slate) with high-contrast, singular accents (e.g
|
|
59
|
+
* **THE LILA BAN:** The "AI Purple/Blue" aesthetic is strictly BANNED. No purple button glows, no neon gradients. Use absolute neutral bases (Zinc/Slate) with high-contrast, singular accents (e.g., Emerald, Electric Blue, or Deep Rose).
|
|
59
60
|
* **COLOR CONSISTENCY:** Stick to one palette for the entire output. Do not fluctuate between warm and cool grays within the same project.
|
|
60
61
|
|
|
61
62
|
**Rule 3: Layout Diversification**
|
|
@@ -106,36 +107,50 @@ To actively combat generic AI designs, systematically implement these high-end c
|
|
|
106
107
|
* **4-7 (Daily App Mode):** Normal spacing for standard web apps.
|
|
107
108
|
* **8-10 (Cockpit Mode):** Tiny paddings. No card boxes; just 1px lines to separate data. Everything is packed. **Mandatory:** Use Monospace (`font-mono`) for all numbers.
|
|
108
109
|
|
|
109
|
-
## 7.
|
|
110
|
-
To guarantee a premium, non-generic output, you MUST strictly avoid these common AI design signatures unless explicitly requested:
|
|
110
|
+
## 7. Don't
|
|
111
111
|
|
|
112
112
|
### Visual & CSS
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
|
|
114
|
+
| Pattern | Replacement | Because |
|
|
115
|
+
|---------|-------------|---------|
|
|
116
|
+
| Neon/outer box-shadow glows | Inner borders or subtle tinted shadows | Glows are an instant AI-design signature |
|
|
117
|
+
| Pure black `#000000` | Off-black, Zinc-950, or Charcoal | Pure black destroys visual depth |
|
|
118
|
+
| Oversaturated accent colors | Desaturated accents blending with neutrals | High-saturation colors look amateurish |
|
|
119
|
+
| Gradient text on large headers | Single solid heading color | Gradient text is an AI design cliché |
|
|
120
|
+
| Custom mouse cursors | Default system cursor | Custom cursors hurt performance and accessibility |
|
|
118
121
|
|
|
119
122
|
### Typography
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
|
|
124
|
+
| Pattern | Replacement | Because |
|
|
125
|
+
|---------|-------------|---------|
|
|
126
|
+
| Inter as display typeface | Geist, Outfit, Cabinet Grotesk, or Satoshi | Inter signals default AI-generated output |
|
|
127
|
+
| Oversized H1 (>40px without reason) | Control hierarchy with weight and color, not just massive scale | Giant headings scream without communicating |
|
|
128
|
+
| Serif fonts on dashboards or data UIs | Sans-serif for data; serif only for editorial/creative contexts | Serifs on dashboards feel out of place |
|
|
123
129
|
|
|
124
130
|
### Layout & Spacing
|
|
125
|
-
* **Align & Space Perfectly:** Ensure padding and margins are mathematically perfect. Avoid floating elements with awkward gaps.
|
|
126
|
-
* **NO 3-Column Card Layouts:** The generic "3 equal cards horizontally" feature row is BANNED. Use a 2-column Zig-Zag, asymmetric grid, or horizontal scrolling approach instead.
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
| Pattern | Replacement | Because |
|
|
133
|
+
|---------|-------------|---------|
|
|
134
|
+
| 3-column identical card layouts | 2-column zig-zag, asymmetric grid, or horizontal scroll | Three equal cards is the #1 AI UI tell |
|
|
135
|
+
| Floating elements with awkward gaps | Mathematically perfect padding and margin alignment | Misaligned spacing reads as sloppy |
|
|
136
|
+
|
|
137
|
+
### Content & Data
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
| Pattern | Replacement | Because |
|
|
140
|
+
|---------|-------------|---------|
|
|
141
|
+
| Generic placeholder names (John Doe, Sarah Chan) | Creative, realistic-sounding names (Dr. Sarah Chen, Marcus Okonkwo) | Generic names feel fake and unprofessional |
|
|
142
|
+
| Emoji avatars or Lucide user icons | Creative photo placeholders or styled SVGs | Emoji avatars degrade perceived quality |
|
|
143
|
+
| Fake or predictable numbers (`99.99%`, `50%`, `$99/mo`) | Organic, messy data (`47.2%`, `$12,450`, `+18.3%`) | Round numbers look fabricated |
|
|
144
|
+
| Startup slop names (Acme, Nexus, SmartFlow) | Premium, contextual brand names | Startup-slop names are a dead AI giveaway |
|
|
145
|
+
| Filler words (Elevate, Seamless, Unleash, Next-Gen) | Concrete, specific verbs and descriptions | AI copywriting clichés destroy credibility |
|
|
146
|
+
| Unsplash URLs in image sources | `picsum.photos/seed/{seed}/800/600` or SVG placeholders | Unsplash links break and leave broken images |
|
|
147
|
+
| Default shadcn/ui appearance | Customized radii, colors, and shadows | Default shadcn reads as AI-generated |
|
|
148
|
+
|
|
149
|
+
### Code Quality
|
|
150
|
+
|
|
151
|
+
| Pattern | Replacement | Because |
|
|
152
|
+
|---------|-------------|---------|
|
|
153
|
+
| Sloppy output — misaligned elements, poor spacing, generic feel | Meticulously refined, visually striking, memorable output | Production-ready cleanliness is non-negotiable for premium UI |
|
|
139
154
|
|
|
140
155
|
## 8. THE CREATIVE ARSENAL (High-End Inspiration)
|
|
141
156
|
Do not default to generic UI. Pull from this library of advanced concepts to ensure the output is visually striking and memorable. When appropriate, leverage **GSAP (ScrollTrigger/Parallax)** for complex scrolltelling or **ThreeJS/WebGL** for 3D/Canvas animations, rather than basic CSS motion. **CRITICAL:** Never mix GSAP/ThreeJS with Framer Motion in the same component tree. Default to Framer Motion for UI/Bento interactions. Use GSAP/ThreeJS EXCLUSIVELY for isolated full-page scrolltelling or canvas backgrounds, wrapped in strict useEffect cleanup blocks.
|
|
@@ -143,7 +158,7 @@ Do not default to generic UI. Pull from this library of advanced concepts to ens
|
|
|
143
158
|
### The Standard Hero Paradigm
|
|
144
159
|
* Stop doing centered text over a dark image. Try asymmetric Hero sections: Text cleanly aligned to the left or right. The background should feature a high-quality, relevant image with a subtle stylistic fade (darkening or lightening gracefully into the background color depending on if it is Light or Dark mode).
|
|
145
160
|
|
|
146
|
-
### Navigation &
|
|
161
|
+
### Navigation & Menus
|
|
147
162
|
* **Mac OS Dock Magnification:** Nav-bar at the edge; icons scale fluidly on hover.
|
|
148
163
|
* **Magnetic Button:** Buttons that physically pull toward the cursor.
|
|
149
164
|
* **Gooey Menu:** Sub-items detach from the main button like a viscous liquid.
|
|
@@ -237,19 +252,15 @@ Evaluate your code against this matrix before outputting. This is the **last** f
|
|
|
237
252
|
- [ ] Are cards omitted in favor of spacing where possible?
|
|
238
253
|
- [ ] Did you strictly isolate CPU-heavy perpetual animations in their own Client Components?
|
|
239
254
|
|
|
240
|
-
##
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
-
|
|
252
|
-
- No explicit font pairing decisions documented
|
|
253
|
-
- Color palette not extracted from project context
|
|
254
|
-
- Components lack hover/focus/active state differentiation
|
|
255
|
-
- Visual hierarchy is flat — everything looks equally important
|
|
255
|
+
## Verification
|
|
256
|
+
|
|
257
|
+
- [ ] Aesthetic baseline config (DESIGN_VARIANCE, MOTION_INTENSITY, VISUAL_DENSITY) respected throughout output
|
|
258
|
+
- [ ] No banned fonts (Inter, Roboto, Arial, system-ui) used as display font
|
|
259
|
+
- [ ] No AI purple/blue gradients, no neon glows, no pure black (`#000000`)
|
|
260
|
+
- [ ] No centered Hero/H1 when DESIGN_VARIANCE > 4
|
|
261
|
+
- [ ] No 3-column identical card layouts
|
|
262
|
+
- [ ] All interactive states (loading, empty, error) implemented
|
|
263
|
+
- [ ] Mobile layout collapse verified at 320px, 768px
|
|
264
|
+
- [ ] `min-h-[100dvh]` used instead of `h-screen` for full-height sections
|
|
265
|
+
- [ ] No emojis in code, markup, or alt text
|
|
266
|
+
- [ ] Perpetual animations isolated in their own Client Components
|
|
@@ -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
|