@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.
Files changed (48) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.pi/VERSION +1 -1
  3. package/dist/template/.pi/extensions/templates-injector.ts +34 -6
  4. package/dist/template/.pi/prompts/INDEX.md +3 -9
  5. package/dist/template/.pi/skills/INDEX.md +81 -19
  6. package/dist/template/.pi/skills/accessibility-audit/SKILL.md +8 -2
  7. package/dist/template/.pi/skills/baseline-ui/SKILL.md +211 -0
  8. package/dist/template/.pi/skills/dcp-hygiene/SKILL.md +1 -1
  9. package/dist/template/.pi/skills/design-taste-frontend/SKILL.md +53 -42
  10. package/dist/template/.pi/skills/fixing-accessibility/SKILL.md +509 -0
  11. package/dist/template/.pi/skills/frontend-design/SKILL.md +60 -47
  12. package/dist/template/.pi/skills/frontend-design/references/animation/motion-advanced.md +88 -15
  13. package/dist/template/.pi/skills/frontend-design/references/animation/motion-core.md +148 -13
  14. package/dist/template/.pi/skills/frontend-design/references/shadcn/setup.md +127 -20
  15. package/dist/template/.pi/skills/frontend-ui-engineering/SKILL.md +21 -27
  16. package/dist/template/.pi/skills/nextjs-app-router/SKILL.md +334 -0
  17. package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -0
  18. package/dist/template/.pi/skills/oklch-color-workflow/SKILL.md +426 -0
  19. package/dist/template/.pi/skills/production-hardening/SKILL.md +652 -0
  20. package/dist/template/.pi/skills/react-best-practices/SKILL.md +79 -1
  21. package/dist/template/.pi/skills/react-compiler/SKILL.md +237 -0
  22. package/dist/template/.pi/skills/react-hook-form/SKILL.md +374 -0
  23. package/dist/template/.pi/skills/react-server-actions/SKILL.md +299 -0
  24. package/dist/template/.pi/skills/shadcn-ui/SKILL.md +404 -0
  25. package/dist/template/.pi/skills/tanstack-query/SKILL.md +330 -0
  26. package/dist/template/.pi/skills/ui-craft-principles/SKILL.md +564 -0
  27. package/dist/template/.pi/skills/ui-quality-audit/SKILL.md +329 -0
  28. package/dist/template/.pi/skills/v0/SKILL.md +264 -0
  29. package/dist/template/.pi/skills/zustand/SKILL.md +333 -0
  30. package/dist/template/.pi/templates/DESIGN.md +76 -0
  31. package/dist/template/.pi/workflows/INDEX.md +2 -1
  32. package/dist/template/.pi/workflows/frontend-feature-workflow.md +343 -0
  33. package/dist/template/.pi/workflows/quality-loop.md +1 -1
  34. package/package.json +1 -1
  35. package/dist/template/.pi/prompts/loop-check.md +0 -87
  36. package/dist/template/.pi/prompts/loop-init.md +0 -157
  37. package/dist/template/.pi/prompts/loop-review.md +0 -90
  38. package/dist/template/.pi/skills/loop-audit/SKILL.md +0 -141
  39. package/dist/template/.pi/skills/loop-cost/SKILL.md +0 -130
  40. package/dist/template/.pi/skills/loop-engineering/SKILL.md +0 -175
  41. package/dist/template/.pi/templates/loop-github-action.yml +0 -162
  42. package/dist/template/.pi/templates/loop-orchestrator.sh +0 -514
  43. package/dist/template/.pi/templates/loop-orchestrator.test.ts +0 -332
  44. package/dist/template/.pi/templates/loop-orchestrator.ts +0 -936
  45. package/dist/template/.pi/templates/loop-state.json +0 -24
  46. package/dist/template/.pi/templates/loop-state.md +0 -98
  47. package/dist/template/.pi/templates/loop-vision.md +0 -110
  48. /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
- # High-Agency Frontend Skill
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. Emerald, Electric Blue, or Deep Rose).
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. AI TELLS (Forbidden Patterns)
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
- * **NO Neon/Outer Glows:** Do not use default `box-shadow` glows or auto-glows. Use inner borders or subtle tinted shadows.
114
- * **NO Pure Black:** Never use `#000000`. Use Off-Black, Zinc-950, or Charcoal.
115
- * **NO Oversaturated Accents:** Desaturate accents to blend elegantly with neutrals.
116
- * **NO Excessive Gradient Text:** Do not use text-fill gradients for large headers.
117
- * **NO Custom Mouse Cursors:** They are outdated and ruin performance/accessibility.
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
- * **NO Inter Font:** Banned. Use `Geist`, `Outfit`, `Cabinet Grotesk`, or `Satoshi`.
121
- * **NO Oversized H1s:** The first heading should not scream. Control hierarchy with weight and color, not just massive scale.
122
- * **Serif Constraints:** Use Serif fonts ONLY for creative/editorial designs. **NEVER** use Serif on clean Dashboards.
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
- ### Content & Data (The "Jane Doe" Effect)
129
- * **NO Generic Names:** "John Doe", "Sarah Chan", or "Jack Su" are banned. Use highly creative, realistic-sounding names.
130
- * **NO Generic Avatars:** DO NOT use standard SVG "egg" or Lucide user icons for avatars. Use creative, believable photo placeholders or specific styling.
131
- * **NO Fake Numbers:** Avoid predictable outputs like `99.99%`, `50%`, or basic phone numbers (`1234567`). Use organic, messy data (`47.2%`, `+1 (312) 847-1928`).
132
- * **NO Startup Slop Names:** "Acme", "Nexus", "SmartFlow". Invent premium, contextual brand names.
133
- * **NO Filler Words:** Avoid AI copywriting clichés like "Elevate", "Seamless", "Unleash", or "Next-Gen". Use concrete verbs.
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
- ### External Resources & Components
136
- * **NO Broken Unsplash Links:** Do not use Unsplash. Use absolute, reliable placeholders like `https://picsum.photos/seed/{random_string}/800/600` or SVG UI Avatars.
137
- * **shadcn/ui Customization:** You may use `shadcn/ui`, but NEVER in its generic default state. You MUST customize the radii, colors, and shadows to match the high-end project aesthetic.
138
- * **Production-Ready Cleanliness:** Code must be extremely clean, visually striking, memorable, and meticulously refined in every detail.
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 & Menüs
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
- ## Common Rationalizations
241
-
242
- | Rationalization | Reality |
243
- |---|---|
244
- | "Default LLM styles are acceptable" | Default LLM styles are generic. Aesthetic intent signals craftsmanship. |
245
- | "Typography doesn't matter for functionality" | Typography is 95% of web design. Bad type ruins even good layouts. |
246
- | "I'll refine the design later" | Design debt compounds like technical debt. Fix it in the first pass. |
247
- | "Users won't notice the details" | Users may not articulate it, but they feel quality. Details accumulate into perception. |
248
-
249
- ## Red Flags
250
-
251
- - Default LLM spacing and typography used without adjustment
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