liteagents 2.5.3 → 2.6.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 (70) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +51 -45
  3. package/installer/cli.js +1 -1
  4. package/installer/installation-engine.js +82 -21
  5. package/installer/package-manager.js +34 -3
  6. package/package.json +3 -2
  7. package/packages/ampcode/AGENT.md +1 -0
  8. package/packages/ampcode/commands/friction/friction.js +19 -4
  9. package/packages/ampcode/commands/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  10. package/packages/ampcode/commands/live-canvas/README.md +273 -0
  11. package/packages/ampcode/commands/live-canvas/templates/.claude/settings.local.json +8 -0
  12. package/packages/ampcode/commands/live-canvas/templates/demo/post-variants.html +205 -0
  13. package/packages/ampcode/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  14. package/packages/ampcode/commands/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  15. package/packages/ampcode/commands/live-canvas/templates/feedback-react/index.ts +62 -0
  16. package/packages/ampcode/commands/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  17. package/packages/ampcode/commands/live-canvas/templates/feedback-react/types.ts +118 -0
  18. package/packages/ampcode/commands/live-canvas/templates/overlay-vanilla.js +477 -0
  19. package/packages/ampcode/commands/live-canvas.md +1104 -0
  20. package/packages/claude/CLAUDE.md +1 -0
  21. package/packages/claude/commands/friction/friction.js +19 -4
  22. package/packages/claude/plugins/live-canvas-marketplace/.claude-plugin/marketplace.json +14 -0
  23. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/.claude-plugin/plugin.json +18 -0
  24. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/README.md +89 -0
  25. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package-lock.json +1142 -0
  26. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package.json +17 -0
  27. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/schema.json +37 -0
  28. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/server.js +179 -0
  29. package/packages/claude/plugins/live-canvas-marketplace/setup.sh +61 -0
  30. package/packages/claude/skills/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  31. package/packages/claude/skills/live-canvas/INTEGRATION_NOTES.md +210 -0
  32. package/packages/claude/skills/live-canvas/README.md +273 -0
  33. package/packages/claude/skills/live-canvas/SKILL.md +1119 -0
  34. package/packages/claude/skills/live-canvas/templates/.claude/settings.local.json +8 -0
  35. package/packages/claude/skills/live-canvas/templates/demo/post-variants.html +205 -0
  36. package/packages/claude/skills/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  37. package/packages/claude/skills/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  38. package/packages/claude/skills/live-canvas/templates/feedback-react/index.ts +62 -0
  39. package/packages/claude/skills/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  40. package/packages/claude/skills/live-canvas/templates/feedback-react/types.ts +118 -0
  41. package/packages/claude/skills/live-canvas/templates/overlay-vanilla.js +477 -0
  42. package/packages/claude/variants.json +2 -1
  43. package/packages/droid/AGENTS.md +1 -0
  44. package/packages/droid/commands/friction/friction.js +19 -4
  45. package/packages/droid/commands/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  46. package/packages/droid/commands/live-canvas/README.md +273 -0
  47. package/packages/droid/commands/live-canvas/templates/.claude/settings.local.json +8 -0
  48. package/packages/droid/commands/live-canvas/templates/demo/post-variants.html +205 -0
  49. package/packages/droid/commands/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  50. package/packages/droid/commands/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  51. package/packages/droid/commands/live-canvas/templates/feedback-react/index.ts +62 -0
  52. package/packages/droid/commands/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  53. package/packages/droid/commands/live-canvas/templates/feedback-react/types.ts +118 -0
  54. package/packages/droid/commands/live-canvas/templates/overlay-vanilla.js +477 -0
  55. package/packages/droid/commands/live-canvas.md +1104 -0
  56. package/packages/opencode/AGENTS.md +1 -0
  57. package/packages/opencode/command/friction/friction.js +19 -4
  58. package/packages/opencode/command/live-canvas/DESIGN_PRINCIPLES.md +2140 -0
  59. package/packages/opencode/command/live-canvas/README.md +273 -0
  60. package/packages/opencode/command/live-canvas/templates/.claude/settings.local.json +8 -0
  61. package/packages/opencode/command/live-canvas/templates/demo/post-variants.html +205 -0
  62. package/packages/opencode/command/live-canvas/templates/feedback-react/FeedbackOverlay.tsx +1299 -0
  63. package/packages/opencode/command/live-canvas/templates/feedback-react/format-utils.ts +283 -0
  64. package/packages/opencode/command/live-canvas/templates/feedback-react/index.ts +62 -0
  65. package/packages/opencode/command/live-canvas/templates/feedback-react/selector-utils.ts +423 -0
  66. package/packages/opencode/command/live-canvas/templates/feedback-react/types.ts +118 -0
  67. package/packages/opencode/command/live-canvas/templates/overlay-vanilla.js +477 -0
  68. package/packages/opencode/command/live-canvas.md +1104 -0
  69. package/packages/opencode/opencode.jsonc +4 -0
  70. package/packages/subagentic-manual.md +15 -12
@@ -0,0 +1,2140 @@
1
+ # Design Principles Reference
2
+
3
+ This document contains curated best practices from world-class designers and design systems. Reference these principles when generating design variations.
4
+
5
+ ---
6
+
7
+ ## Part 1: UX Foundations
8
+
9
+ ### Jakob Nielsen's 10 Usability Heuristics
10
+
11
+ 1. **Visibility of system status** - Always keep users informed through appropriate feedback within reasonable time
12
+ 2. **Match between system and real world** - Use familiar language, concepts, and conventions
13
+ 3. **User control and freedom** - Provide clear "emergency exits" (undo, cancel, back)
14
+ 4. **Consistency and standards** - Follow platform conventions; same words mean same things
15
+ 5. **Error prevention** - Eliminate error-prone conditions or ask for confirmation
16
+ 6. **Recognition over recall** - Minimize memory load; make options visible
17
+ 7. **Flexibility and efficiency** - Provide accelerators for expert users (shortcuts, defaults)
18
+ 8. **Aesthetic and minimalist design** - Remove irrelevant information; every element competes
19
+ 9. **Help users recover from errors** - Plain language errors with constructive solutions
20
+ 10. **Help and documentation** - Provide concise, task-focused help when needed
21
+
22
+ ### Don Norman's Design Principles
23
+
24
+ - **Affordances** - Design elements should suggest their usage
25
+ - **Signifiers** - Visual cues that indicate where actions should happen
26
+ - **Mapping** - Controls should relate spatially to their effects
27
+ - **Feedback** - Every action needs a perceivable response
28
+ - **Conceptual model** - Users should understand how the system works
29
+
30
+ ### Cognitive Load Principles
31
+
32
+ - **Limit choices** - 5-7 items max in navigation; 3-4 options in decisions
33
+ - **Progressive disclosure** - Show only what's needed at each step
34
+ - **Chunking** - Group related items; break long forms into steps
35
+ - **Visual hierarchy** - Guide attention with size, color, contrast, position
36
+ - **Reduce cognitive friction** - Minimize decisions, clicks, and reading
37
+
38
+ ### URL & State Principles
39
+
40
+ - **URL state reflection** - Important UI state (filters, tabs, pagination) should be in the URL
41
+ - **Shareable links** - Users should be able to share/bookmark the current view
42
+ - **Browser navigation** - Back/forward buttons should work as expected
43
+
44
+ ### Destructive Actions
45
+
46
+ - **Confirmation required** - Delete, remove, and irreversible actions need explicit confirmation
47
+ - **Clear consequences** - State exactly what will happen ("This will permanently delete 5 files")
48
+ - **Recovery path** - Prefer soft delete with undo over immediate permanent deletion
49
+ - **Visual distinction** - Destructive buttons use warning colors (red) and distinct styling
50
+
51
+ ---
52
+
53
+ ## Part 2: Visual Design Systems
54
+
55
+ ### Typography (from iA, Stripe, Linear)
56
+
57
+ **Hierarchy:**
58
+
59
+ ```
60
+ Display: 32-48px, -0.02em tracking, 700 weight
61
+ Heading 1: 24-32px, -0.02em tracking, 600 weight
62
+ Heading 2: 20-24px, -0.01em tracking, 600 weight
63
+ Heading 3: 16-18px, normal tracking, 600 weight
64
+ Body: 14-16px, normal tracking, 400 weight
65
+ Caption: 12-13px, +0.01em tracking, 400-500 weight
66
+ ```
67
+
68
+ **Best practices:**
69
+
70
+ - Max 60-75 characters per line for readability
71
+ - Line height: 1.4-1.6 for body text, 1.2-1.3 for headings
72
+ - Use weight contrast (400 vs 600) more than size contrast
73
+ - Limit to 2 font families maximum
74
+ - System fonts for performance: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
75
+
76
+ **Typographic details:**
77
+
78
+ - Use proper ellipsis `…` not `...` (three dots)
79
+ - Use curly quotes `"` `"` not straight quotes `"`
80
+ - Non-breaking spaces for values: `10 MB`, `5 items` (use ` ` or `\u00A0`)
81
+ - `font-variant-numeric: tabular-nums` for numbers in tables, counters, prices
82
+ - `text-wrap: balance` for headings (prevents orphans/widows)
83
+ - `text-wrap: pretty` for body text (better line breaks)
84
+
85
+ ### Spacing System (8px grid)
86
+
87
+ ```
88
+ 4px - Tight: icon padding, inline spacing
89
+ 8px - Base: related elements, form field padding
90
+ 12px - Comfortable: between form fields
91
+ 16px - Standard: section padding, card padding
92
+ 24px - Relaxed: between sections
93
+ 32px - Spacious: major section breaks
94
+ 48px - Generous: page section separation
95
+ 64px+ - Hero: landing page sections
96
+ ```
97
+
98
+ **Spacing principles:**
99
+
100
+ - Related items closer together (Gestalt proximity)
101
+ - Consistent internal padding (all sides equal, or vertical > horizontal)
102
+ - White space is not wasted space—it creates focus
103
+ - Touch targets minimum 44x44px (Apple HIG)
104
+
105
+ ### Color (from Stripe, Linear, Vercel)
106
+
107
+ **Neutral foundation:**
108
+
109
+ ```
110
+ Background: #FFFFFF / #000000 (dark)
111
+ Surface: #FAFAFA / #111111 (dark)
112
+ Border: #E5E5E5 / #333333 (dark)
113
+ Text primary: #171717 / #EDEDED (dark)
114
+ Text secondary: #737373 / #A3A3A3 (dark)
115
+ Text tertiary: #A3A3A3 / #737373 (dark)
116
+ ```
117
+
118
+ **Accent usage:**
119
+
120
+ - Primary action: single brand color, used sparingly
121
+ - Interactive elements: consistent color for all clickable items
122
+ - Semantic colors: red (error), green (success), yellow (warning), blue (info)
123
+ - Hover states: 10% darker or add subtle background
124
+ - Focus states: 2px ring with offset, high contrast
125
+
126
+ **Color principles:**
127
+
128
+ - WCAG AA minimum: 4.5:1 for text, 3:1 for UI elements
129
+ - One primary accent color; avoid rainbow interfaces
130
+ - Use opacity for secondary states (hover, disabled)
131
+ - Dark mode: don't just invert—reduce contrast, use darker surfaces
132
+
133
+ **Dark mode setup:**
134
+
135
+ ```html
136
+ <!-- On <html> element -->
137
+ <html class="dark" style="color-scheme: dark">
138
+
139
+ <!-- Theme color matching page background -->
140
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
141
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
142
+ ```
143
+
144
+ ### Content Handling
145
+
146
+ **Text truncation:**
147
+
148
+ ```css
149
+ /* Single line truncation */
150
+ .truncate {
151
+ overflow: hidden;
152
+ text-overflow: ellipsis;
153
+ white-space: nowrap;
154
+ }
155
+
156
+ /* Multi-line truncation */
157
+ .line-clamp-2 {
158
+ display: -webkit-box;
159
+ -webkit-box-orient: vertical;
160
+ -webkit-line-clamp: 2;
161
+ overflow: hidden;
162
+ }
163
+
164
+ /* Break long words */
165
+ .break-words {
166
+ overflow-wrap: break-word;
167
+ word-break: break-word;
168
+ }
169
+ ```
170
+
171
+ **Flex children with text:**
172
+
173
+ ```css
174
+ /* IMPORTANT: Flex children with text need min-w-0 to truncate properly */
175
+ .flex-child-with-text {
176
+ min-width: 0; /* Allows text to shrink below content size */
177
+ }
178
+ ```
179
+
180
+ **Empty states:**
181
+
182
+ - Always design the empty state—it's the first thing users see
183
+ - Include helpful message + primary action
184
+ - Use illustration or icon to add visual interest
185
+
186
+ **Images:**
187
+
188
+ ```jsx
189
+ // Always include explicit dimensions to prevent layout shift
190
+ <img
191
+ src="/image.jpg"
192
+ width={800}
193
+ height={600}
194
+ alt="Description"
195
+ loading="lazy" // Defer off-screen images
196
+ />
197
+
198
+ // For above-the-fold images
199
+ <img src="/hero.jpg" width={1200} height={800} alt="Hero" priority />
200
+ ```
201
+
202
+ ### Border Radius (from modern SaaS)
203
+
204
+ ```
205
+ None (0px): Tables, dividers, full-bleed images
206
+ Small (4px): Buttons, inputs, tags, badges
207
+ Medium (8px): Cards, modals, dropdowns
208
+ Large (12px): Feature cards, hero elements
209
+ Full (9999px): Avatars, pills, toggle tracks
210
+ ```
211
+
212
+ **Principles:**
213
+
214
+ - Consistency: pick 2-3 radius values and stick to them
215
+ - Nested elements: inner radius = outer radius - padding
216
+ - Sharp corners feel technical/precise; round feels friendly/approachable
217
+
218
+ ### Shadows & Elevation (from Material, Linear)
219
+
220
+ ```
221
+ Level 0: none (flat, on surface)
222
+ Level 1: 0 1px 2px rgba(0,0,0,0.05) - Subtle lift (cards)
223
+ Level 2: 0 4px 6px rgba(0,0,0,0.07) - Raised (dropdowns)
224
+ Level 3: 0 10px 15px rgba(0,0,0,0.1) - Floating (modals)
225
+ Level 4: 0 20px 25px rgba(0,0,0,0.15) - High (popovers)
226
+ ```
227
+
228
+ **Principles:**
229
+
230
+ - Shadows should feel like natural light (top-down, slight offset)
231
+ - Dark mode: use lighter surface colors instead of shadows
232
+ - Combine with subtle border for definition
233
+ - Interactive elements can elevate on hover
234
+
235
+ ---
236
+
237
+ ## Part 3: Component Patterns
238
+
239
+ ### Buttons (from Stripe, Linear)
240
+
241
+ **Hierarchy:**
242
+
243
+ 1. **Primary** - One per view, main action, filled with brand color
244
+ 2. **Secondary** - Supporting actions, outlined or ghost style
245
+ 3. **Tertiary** - Low-emphasis actions, text-only with hover state
246
+ 4. **Destructive** - Delete/remove actions, red with confirmation
247
+
248
+ **States:**
249
+
250
+ - Default → Hover (+shadow or darken) → Active (scale 0.97) → Disabled (50% opacity)
251
+ - Loading: replace text with spinner, maintain width
252
+ - Min width: 80px; min height: 36px (touch-friendly: 44px)
253
+
254
+ **Best practices:**
255
+
256
+ - **Specific labels:** "Save API Key" not "Continue" or "Submit"
257
+ - Verb + noun labels: "Create project" not "Create"
258
+ - Sentence case, not ALL CAPS
259
+ - Icon left of text (or icon-only with tooltip)
260
+ - Primary button right-aligned in forms/dialogs
261
+ - **Icon buttons require `aria-label`**
262
+
263
+ **Active state feedback:**
264
+
265
+ ```css
266
+ button:active {
267
+ transform: scale(0.97);
268
+ }
269
+ ```
270
+
271
+ ### Forms (from Airbnb, Stripe, Vercel)
272
+
273
+ **Input anatomy:**
274
+
275
+ ```
276
+ ┌─────────────────────────────────┐
277
+ │ Label │ ← Required (above input, not inside)
278
+ │ ┌─────────────────────────────┐ │
279
+ │ │ Placeholder... │ │ ← Format hint only, ends with ...
280
+ │ └─────────────────────────────┘ │
281
+ │ Helper text or error message │ ← Specific and actionable
282
+ └─────────────────────────────────┘
283
+ ```
284
+
285
+ **Autocomplete attributes (required):**
286
+
287
+ ```html
288
+ <!-- Always use appropriate autocomplete for user data -->
289
+ <input type="email" autocomplete="email" />
290
+ <input type="text" autocomplete="name" />
291
+ <input type="text" autocomplete="given-name" />
292
+ <input type="text" autocomplete="family-name" />
293
+ <input type="text" autocomplete="organization" />
294
+ <input type="text" autocomplete="street-address" />
295
+ <input type="text" autocomplete="postal-code" />
296
+ <input type="tel" autocomplete="tel" />
297
+ <input type="password" autocomplete="current-password" />
298
+ <input type="password" autocomplete="new-password" />
299
+ <input type="text" autocomplete="one-time-code" />
300
+ ```
301
+
302
+ **Input types and modes:**
303
+
304
+ ```html
305
+ <!-- Use correct type for validation and keyboard -->
306
+ <input type="email" inputmode="email" />
307
+ <input type="tel" inputmode="tel" />
308
+ <input type="url" inputmode="url" />
309
+ <input type="number" inputmode="numeric" />
310
+
311
+ <!-- Numeric input without spinners -->
312
+ <input type="text" inputmode="numeric" pattern="[0-9]*" />
313
+ ```
314
+
315
+ **Disable spellcheck where inappropriate:**
316
+
317
+ ```jsx
318
+ // Disable for codes, emails, usernames, URLs
319
+ <input type="text" spellCheck={false} autoComplete="username" />
320
+ <input type="email" spellCheck={false} />
321
+ <input type="text" spellCheck={false} placeholder="Enter code..." />
322
+ ```
323
+
324
+ **Anti-patterns to avoid:**
325
+
326
+ ```jsx
327
+ // NEVER block paste - this is hostile UX
328
+ <input onPaste={(e) => e.preventDefault()} /> // ❌ NEVER DO THIS
329
+
330
+ // NEVER use placeholder as label
331
+ <input placeholder="Email" /> // ❌ Placeholder disappears on focus
332
+
333
+ // NEVER validate on every keystroke
334
+ onChange={(e) => validateEmail(e.target.value)} // ❌ Too aggressive
335
+ ```
336
+
337
+ **Best practices:**
338
+
339
+ - Labels above inputs (not inside—accessibility)
340
+ - Placeholder ≠ label; use for format hints only, end with `...`
341
+ - Inline validation on blur, not on every keystroke
342
+ - Error messages: specific and actionable ("Email must include @")
343
+ - **Focus first error field** after form submission fails
344
+ - Success state: checkmark icon, green border (brief)
345
+ - Required fields: mark optional ones instead of required
346
+ - Single column forms outperform multi-column
347
+
348
+ **Unsaved changes warning:**
349
+
350
+ ```jsx
351
+ // Warn users before leaving with unsaved changes
352
+ useEffect(() => {
353
+ const handleBeforeUnload = (e) => {
354
+ if (hasUnsavedChanges) {
355
+ e.preventDefault();
356
+ e.returnValue = '';
357
+ }
358
+ };
359
+ window.addEventListener('beforeunload', handleBeforeUnload);
360
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload);
361
+ }, [hasUnsavedChanges]);
362
+ ```
363
+
364
+ ### Cards (from Material, Apple)
365
+
366
+ **Anatomy:**
367
+
368
+ ```
369
+ ┌────────────────────────────────┐
370
+ │ [Media/Image] │ ← Optional
371
+ ├────────────────────────────────┤
372
+ │ Eyebrow · Metadata │ ← Optional
373
+ │ Title │ ← Required
374
+ │ Description text that can │ ← Optional
375
+ │ wrap to multiple lines... │
376
+ ├────────────────────────────────┤
377
+ │ [Actions] [More] │ ← Optional
378
+ └────────────────────────────────┘
379
+ ```
380
+
381
+ **Best practices:**
382
+
383
+ - Entire card clickable for primary action
384
+ - Consistent padding (16-24px)
385
+ - Image aspect ratios: 16:9, 4:3, 1:1 (be consistent)
386
+ - Limit to 2 actions max; overflow to menu
387
+ - Hover: subtle lift (translateY -2px + shadow increase)
388
+
389
+ ### Tables (from Linear, Notion)
390
+
391
+ **Best practices:**
392
+
393
+ - Left-align text, right-align numbers
394
+ - **Use `tabular-nums` for numeric columns** (consistent width digits)
395
+ - Zebra striping OR row hover, not both
396
+ - Sticky header on scroll
397
+ - Sortable columns: show current sort indicator
398
+ - Actions: row hover reveals action buttons (or kebab menu)
399
+ - Empty state: helpful message + action
400
+ - Pagination vs infinite scroll: pagination for data accuracy, infinite for browsing
401
+ - Min row height: 48px for touch; 40px for dense
402
+ - **Virtualize tables with >50 rows**
403
+
404
+ ```css
405
+ .numeric-column {
406
+ font-variant-numeric: tabular-nums;
407
+ text-align: right;
408
+ }
409
+ ```
410
+
411
+ ### Navigation (from Apple HIG, Material)
412
+
413
+ **Patterns by scale:**
414
+
415
+ - **2-5 items**: Tab bar / horizontal tabs
416
+ - **5-10 items**: Side navigation (collapsible)
417
+ - **10+ items**: Side nav with sections/groups
418
+
419
+ **Best practices:**
420
+
421
+ - Current location always visible
422
+ - Breadcrumbs for deep hierarchy (not for flat structures)
423
+ - Mobile: bottom nav for primary actions (thumb-friendly)
424
+ - Icons + labels together; icon-only needs tooltip
425
+ - Consistent order across pages
426
+
427
+ ---
428
+
429
+ ## Part 4: Interaction Design
430
+
431
+ ### Feedback Patterns (from Dan Saffer's Microinteractions)
432
+
433
+ **Every action needs feedback:**
434
+
435
+ 1. **Immediate** - Button press visual (scale, color change)
436
+ 2. **Progress** - Loading states for anything >1s
437
+ 3. **Completion** - Success confirmation (toast, checkmark, animation)
438
+ 4. **Failure** - Clear error with recovery path
439
+
440
+ **Loading states:**
441
+
442
+ - 0-100ms: No indicator needed
443
+ - 100-300ms: Subtle change (opacity, skeleton)
444
+ - 300ms-1s: Spinner or progress bar
445
+ - 1s+: Skeleton screens + progress indication
446
+ - 10s+: Background processing with notification
447
+
448
+ ### State Handling
449
+
450
+ **Every component needs these states:**
451
+
452
+ ```
453
+ Default → Base appearance
454
+ Hover → Interactive hint (cursor change, highlight)
455
+ Focus → Keyboard navigation (visible ring)
456
+ Active → Being pressed/activated
457
+ Loading → Async operation in progress
458
+ Disabled → Not available (reduce opacity, remove pointer)
459
+ Error → Invalid input or failed operation
460
+ Success → Completed successfully (brief)
461
+ Empty → No data to display (helpful message + action)
462
+ ```
463
+
464
+ ### Touch & Pointer Interactions
465
+
466
+ **Faster tap response:**
467
+
468
+ ```css
469
+ /* Remove 300ms tap delay on touch devices */
470
+ button, a, [role="button"] {
471
+ touch-action: manipulation;
472
+ }
473
+ ```
474
+
475
+ **Contain scroll in modals:**
476
+
477
+ ```css
478
+ /* Prevent scroll chaining to body when modal/drawer reaches edge */
479
+ .modal, .drawer, .dropdown {
480
+ overscroll-behavior: contain;
481
+ }
482
+ ```
483
+
484
+ **Touch targets:**
485
+
486
+ - Minimum 44x44px for all interactive elements (Apple HIG)
487
+ - Provide adequate spacing between targets (8px minimum)
488
+
489
+ **Hover states for pointer devices only:**
490
+
491
+ ```css
492
+ /* Only apply hover effects on devices with fine pointers */
493
+ @media (hover: hover) and (pointer: fine) {
494
+ .card:hover {
495
+ transform: translateY(-2px);
496
+ box-shadow: var(--shadow-lg);
497
+ }
498
+ }
499
+ ```
500
+
501
+ **Tap highlight:**
502
+
503
+ ```css
504
+ /* Customize or remove tap highlight on mobile */
505
+ button {
506
+ -webkit-tap-highlight-color: transparent; /* Remove default */
507
+ /* Or use a custom color */
508
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
509
+ }
510
+ ```
511
+
512
+ ### Optimistic Updates (from Linear, Notion)
513
+
514
+ - Update UI immediately, sync in background
515
+ - Show subtle "Saving..." indicator
516
+ - On failure: revert UI + show error toast with retry
517
+ - Best for: toggles, reordering, text edits
518
+ - Avoid for: destructive actions, payments
519
+
520
+ ### Progressive Disclosure
521
+
522
+ **Reveal complexity gradually:**
523
+
524
+ - Show essential options first
525
+ - "Advanced" or "More options" for power features
526
+ - Inline expansion over page navigation
527
+ - Tooltips for supplementary information
528
+ - Context menus for secondary actions
529
+
530
+ ### Inferring Intent
531
+
532
+ **Anticipate user actions before they happen:**
533
+
534
+ ```jsx
535
+ // Preload on mousedown (fires before click)
536
+ <button
537
+ onMouseDown={() => prefetchData()}
538
+ onClick={() => showData()}
539
+ >
540
+ View Details
541
+ </button>
542
+
543
+ // Preload on hover for links
544
+ <Link
545
+ href="/dashboard"
546
+ onMouseEnter={() => router.prefetch('/dashboard')}
547
+ >
548
+ Dashboard
549
+ </Link>
550
+ ```
551
+
552
+ **Proximity-based preloading:**
553
+
554
+ ```jsx
555
+ // Start loading when cursor approaches
556
+ function useProximityPreload(ref, onApproach) {
557
+ useEffect(() => {
558
+ const element = ref.current;
559
+ const handleMouseMove = (e) => {
560
+ const rect = element.getBoundingClientRect();
561
+ const distance = Math.hypot(
562
+ e.clientX - (rect.left + rect.width / 2),
563
+ e.clientY - (rect.top + rect.height / 2)
564
+ );
565
+ if (distance < 100) onApproach();
566
+ };
567
+ document.addEventListener('mousemove', handleMouseMove);
568
+ return () => document.removeEventListener('mousemove', handleMouseMove);
569
+ }, [ref, onApproach]);
570
+ }
571
+ ```
572
+
573
+ **Smart defaults:**
574
+
575
+ - Pre-fill forms with likely values
576
+ - Remember user's last selection
577
+ - Use geolocation for location fields
578
+ - Default date pickers to sensible dates (today, tomorrow)
579
+
580
+ ### Interaction Metaphors
581
+
582
+ **Physical analogies users already understand:**
583
+
584
+ | Gesture | Real-world Metaphor | UI Behavior |
585
+ |---------|---------------------|-------------|
586
+ | Drag | Moving physical objects | Reorder, move items |
587
+ | Swipe | Flipping pages, pushing aside | Navigate, dismiss |
588
+ | Pinch | Zooming a camera lens | Scale content |
589
+ | Pull down | Stretching a spring | Refresh content |
590
+ | Long press | Pressing firmly to reveal | Context menu |
591
+
592
+ **Consistency requirement:**
593
+
594
+ Once you establish a gesture metaphor, use it consistently:
595
+
596
+ ```
597
+ ❌ Swipe right to delete in one view, swipe right to archive in another
598
+ ✅ Swipe right always archives, swipe left always deletes
599
+ ```
600
+
601
+ **Honor platform conventions:**
602
+
603
+ - iOS: Swipe from left edge = back navigation
604
+ - Android: Back button/gesture = return to previous screen
605
+ - Desktop: Right-click = context menu
606
+
607
+ ### Ergonomic Interactions
608
+
609
+ **Expand hit areas with pseudo-elements:**
610
+
611
+ ```css
612
+ /* Thin visual element with large tap target */
613
+ .icon-button {
614
+ position: relative;
615
+ width: 24px;
616
+ height: 24px;
617
+ }
618
+
619
+ .icon-button::after {
620
+ content: '';
621
+ position: absolute;
622
+ inset: -12px; /* Expands hit area to 48x48px */
623
+ }
624
+ ```
625
+
626
+ **Bidirectional scroll support:**
627
+
628
+ ```css
629
+ /* Support both LTR and RTL scrolling */
630
+ .horizontal-scroll {
631
+ overflow-x: auto;
632
+ scroll-behavior: smooth;
633
+ /* Use logical properties */
634
+ scroll-padding-inline: 16px;
635
+ }
636
+ ```
637
+
638
+ **Thumb-friendly mobile zones:**
639
+
640
+ ```
641
+ ┌─────────────────────────────────┐
642
+ │ Hard to reach (top) │ ← Avoid primary actions here
643
+ ├─────────────────────────────────┤
644
+ │ │
645
+ │ Comfortable middle │ ← Secondary actions OK
646
+ │ │
647
+ ├─────────────────────────────────┤
648
+ │ Easy reach (bottom) │ ← Primary actions here
649
+ └─────────────────────────────────┘
650
+ ```
651
+
652
+ ### Contained Gestures
653
+
654
+ **Prevent gesture conflicts with parent elements:**
655
+
656
+ ```css
657
+ /* Contain drag/swipe gestures within element */
658
+ .draggable-area {
659
+ touch-action: none; /* Disable browser handling */
660
+ user-select: none; /* Prevent text selection during drag */
661
+ }
662
+
663
+ /* Allow vertical scroll but capture horizontal */
664
+ .horizontal-swipe {
665
+ touch-action: pan-y; /* Allow vertical, capture horizontal */
666
+ }
667
+ ```
668
+
669
+ **Pointer capture for drag operations:**
670
+
671
+ ```jsx
672
+ function useDrag(onDrag, onDragEnd) {
673
+ const handlePointerDown = (e) => {
674
+ e.currentTarget.setPointerCapture(e.pointerId);
675
+ };
676
+
677
+ const handlePointerMove = (e) => {
678
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
679
+ onDrag({ x: e.clientX, y: e.clientY });
680
+ }
681
+ };
682
+
683
+ const handlePointerUp = (e) => {
684
+ e.currentTarget.releasePointerCapture(e.pointerId);
685
+ onDragEnd();
686
+ };
687
+
688
+ return {
689
+ onPointerDown: handlePointerDown,
690
+ onPointerMove: handlePointerMove,
691
+ onPointerUp: handlePointerUp,
692
+ };
693
+ }
694
+ ```
695
+
696
+ **Drag threshold detection:**
697
+
698
+ ```jsx
699
+ // Distinguish click from drag with movement threshold
700
+ const DRAG_THRESHOLD = 5; // pixels
701
+
702
+ function useDragThreshold() {
703
+ const startPos = useRef(null);
704
+ const [isDragging, setIsDragging] = useState(false);
705
+
706
+ const handlePointerDown = (e) => {
707
+ startPos.current = { x: e.clientX, y: e.clientY };
708
+ };
709
+
710
+ const handlePointerMove = (e) => {
711
+ if (!startPos.current) return;
712
+
713
+ const distance = Math.hypot(
714
+ e.clientX - startPos.current.x,
715
+ e.clientY - startPos.current.y
716
+ );
717
+
718
+ if (distance > DRAG_THRESHOLD) {
719
+ setIsDragging(true);
720
+ }
721
+ };
722
+
723
+ const handlePointerUp = (e) => {
724
+ const wasDragging = isDragging;
725
+ setIsDragging(false);
726
+ startPos.current = null;
727
+ return wasDragging; // Return true if was drag, false if was click
728
+ };
729
+
730
+ return { isDragging, handlePointerDown, handlePointerMove, handlePointerUp };
731
+ }
732
+ ```
733
+
734
+ **Gesture state machine:**
735
+
736
+ ```
737
+ IDLE → PRESS (pointer down)
738
+ PRESS → DRAG (movement > threshold)
739
+ PRESS → CLICK (pointer up, no movement)
740
+ DRAG → DRAG_END (pointer up)
741
+ DRAG_END → IDLE (animation complete)
742
+ ```
743
+
744
+ ---
745
+
746
+ ## Part 5: Motion & Animation
747
+
748
+ ### The Frequency Principle
749
+
750
+ Animation frequency should match usage frequency:
751
+
752
+ | Usage Pattern | Animation Approach |
753
+ |---|---|
754
+ | 100+ times/day | No animation—instant response |
755
+ | Occasional use | Standard animation (150-300ms) |
756
+ | Rare/first-time | Can add delight, longer duration |
757
+
758
+ Example: A "send message" button used constantly → instant. An "export report" button used weekly → can animate.
759
+
760
+ ### Easing Blueprint
761
+
762
+ **Ease-out family (most common):**
763
+
764
+ Use for entrances, user-initiated actions, and most UI transitions.
765
+
766
+ ```css
767
+ :root {
768
+ /* Increasing intensity: quad → cubic → quart → quint */
769
+ --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
770
+ --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
771
+ --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
772
+ --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
773
+ }
774
+ ```
775
+
776
+ **Ease-in-out family:**
777
+
778
+ Use for on-screen movement (element moving from point A to point B).
779
+
780
+ ```css
781
+ :root {
782
+ --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
783
+ --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
784
+ }
785
+ ```
786
+
787
+ **Easing decision flowchart:**
788
+
789
+ ```
790
+ Is the element entering or exiting the screen?
791
+ → Yes: Use ease-out (for both enter AND exit)
792
+
793
+ Is the element moving on screen (A to B)?
794
+ → Yes: Use ease-in-out
795
+
796
+ Is it a hover state or color change?
797
+ → Yes: Use ease (CSS default) or ease-out-quad
798
+
799
+ Is it constant/looping motion (spinner, progress)?
800
+ → Yes: Use linear
801
+ ```
802
+
803
+ ### Timing Guidelines
804
+
805
+ | Element Type | Duration | Notes |
806
+ |---|---|---|
807
+ | Micro-interactions | 100-150ms | Buttons, toggles, hover states |
808
+ | Tooltips, dropdowns | 150-250ms | Small UI appearing |
809
+ | Modals, drawers | 200-300ms | Larger surfaces |
810
+ | Page transitions | 300-400ms | Full view changes |
811
+ | Staggered items | 30-50ms delay | Between each item |
812
+
813
+ **Important:** Exit animations should be 20-30% faster than entrances.
814
+
815
+ ### Animation Patterns
816
+
817
+ **Entrances:**
818
+
819
+ - Fade in + slide up (8-16px)
820
+ - Scale from 0.95 to 1 + fade (never from 0)
821
+ - Stagger children by 30-50ms
822
+
823
+ **Exits:**
824
+
825
+ - Fade out (faster than entrance)
826
+ - Scale to 0.95 + fade
827
+ - Slide in direction of dismissal
828
+
829
+ **Transform origin:**
830
+
831
+ Always set `transform-origin` toward the trigger element:
832
+
833
+ ```css
834
+ /* Dropdown opening from button */
835
+ .dropdown {
836
+ transform-origin: top left; /* Opens from button location */
837
+ }
838
+
839
+ /* Modal opening from center */
840
+ .modal {
841
+ transform-origin: center center;
842
+ }
843
+ ```
844
+
845
+ **Hover flicker prevention:**
846
+
847
+ ```css
848
+ /* ❌ Don't animate the parent on hover */
849
+ .card:hover {
850
+ transform: scale(1.02); /* Causes flicker */
851
+ }
852
+
853
+ /* ✅ Animate a child element instead */
854
+ .card:hover .card-content {
855
+ transform: scale(1.02);
856
+ }
857
+ ```
858
+
859
+ **Sequential tooltips:**
860
+
861
+ After the first tooltip in a series, skip animation for subsequent ones:
862
+
863
+ ```jsx
864
+ // Skip animation if another tooltip was shown recently
865
+ const skipAnimation = Date.now() - lastTooltipTime < 300;
866
+ ```
867
+
868
+ ### Spring Physics
869
+
870
+ **When to use springs:**
871
+
872
+ - Drag and drop interactions
873
+ - Gesture-based animations
874
+ - Interruptible motion (user can grab mid-animation)
875
+ - Physics-based feel (natural, organic)
876
+
877
+ **Spring parameters:**
878
+
879
+ ```jsx
880
+ // Physical spring configuration
881
+ const spring = {
882
+ stiffness: 300, // Higher = faster, snappier
883
+ damping: 30, // Higher = less oscillation
884
+ mass: 1 // Higher = more inertia, slower
885
+ };
886
+
887
+ // Typical ranges:
888
+ // stiffness: 100-1000 (most UI: 200-400)
889
+ // damping: 10-100 (most UI: 20-40)
890
+ // mass: 0.5-2 (most UI: 1)
891
+ ```
892
+
893
+ **Critical principle: Never reuse spring values**
894
+
895
+ Each interaction should have its own tuned spring. A dropdown menu spring differs from a drag-to-dismiss spring.
896
+
897
+ ```jsx
898
+ // ❌ Bad - same spring for everything
899
+ const SPRING = { stiffness: 300, damping: 30 };
900
+
901
+ // ✅ Good - tuned per interaction
902
+ const DROPDOWN_SPRING = { stiffness: 400, damping: 35, mass: 0.8 };
903
+ const DRAG_SPRING = { stiffness: 250, damping: 25, mass: 1 };
904
+ const BOUNCE_SPRING = { stiffness: 180, damping: 12, mass: 1 };
905
+ ```
906
+
907
+ **Damping for rubber band effects:**
908
+
909
+ ```jsx
910
+ // Rubber band effect for over-scroll
911
+ function rubberBand(offset, limit, elasticity = 0.55) {
912
+ const clampedOffset = Math.max(0, offset);
913
+ const delta = clampedOffset - limit;
914
+ if (delta <= 0) return offset;
915
+
916
+ // Logarithmic decay for natural feel
917
+ return limit + (1 - Math.exp(-delta / (limit * elasticity))) * limit * elasticity;
918
+ }
919
+ ```
920
+
921
+ **iOS-style projection (momentum scrolling):**
922
+
923
+ ```jsx
924
+ // Project final position based on velocity
925
+ function project(velocity, position, deceleration = 0.998) {
926
+ // v(t) = v0 * deceleration^t
927
+ // When v(t) ≈ 0, t = log(0.001) / log(deceleration)
928
+ const duration = Math.log(0.001) / Math.log(deceleration);
929
+ const distance = velocity * (1 - Math.pow(deceleration, duration)) / (1 - deceleration);
930
+ return position + distance;
931
+ }
932
+ ```
933
+
934
+ **Framer Motion spring shorthand:**
935
+
936
+ ```jsx
937
+ // Simple configuration
938
+ const springConfig = {
939
+ type: "spring",
940
+ duration: 0.5, // Overall duration
941
+ bounce: 0.2 // 0 = no bounce, 1 = very bouncy
942
+ };
943
+
944
+ // Subtle bounce (most UI): 0.1 - 0.3
945
+ // Playful bounce: 0.3 - 0.5
946
+ // Avoid > 0.5 in most production UI
947
+ ```
948
+
949
+ ### Motion Choreography
950
+
951
+ **Blur overlapping layers:**
952
+
953
+ When animated elements cross paths, they create visual noise. Add subtle blur:
954
+
955
+ ```css
956
+ /* Add 1-2px blur during transitions */
957
+ .transitioning-element {
958
+ filter: blur(1px);
959
+ }
960
+
961
+ /* Or use will-change to hint GPU compositing */
962
+ .animated-layer {
963
+ will-change: transform;
964
+ transform: translateZ(0); /* Force separate layer */
965
+ }
966
+ ```
967
+
968
+ **Stagger animation delays:**
969
+
970
+ ```jsx
971
+ // Stagger children by 30-50ms each
972
+ const container = {
973
+ hidden: { opacity: 0 },
974
+ show: {
975
+ opacity: 1,
976
+ transition: {
977
+ staggerChildren: 0.04, // 40ms between each
978
+ delayChildren: 0.1, // Wait 100ms before starting
979
+ }
980
+ }
981
+ };
982
+
983
+ const item = {
984
+ hidden: { opacity: 0, y: 8 },
985
+ show: { opacity: 1, y: 0 }
986
+ };
987
+ ```
988
+
989
+ **Double exit stiffness:**
990
+
991
+ Exit animations should feel quicker. Double the spring stiffness:
992
+
993
+ ```jsx
994
+ function AnimatedPanel({ isOpen }) {
995
+ return (
996
+ <motion.div
997
+ initial={{ opacity: 0, scale: 0.95 }}
998
+ animate={{ opacity: 1, scale: 1 }}
999
+ exit={{ opacity: 0, scale: 0.95 }}
1000
+ transition={{
1001
+ enter: { type: "spring", stiffness: 300, damping: 30 },
1002
+ exit: { type: "spring", stiffness: 600, damping: 30 } // 2x stiffness
1003
+ }}
1004
+ />
1005
+ );
1006
+ }
1007
+ ```
1008
+
1009
+ **Crossfade icons (not swap):**
1010
+
1011
+ When changing icons, don't just swap. Scale down + blur out the old, scale up + blur in the new:
1012
+
1013
+ ```jsx
1014
+ // Icon crossfade
1015
+ <AnimatePresence mode="wait">
1016
+ <motion.div
1017
+ key={iconKey}
1018
+ initial={{ opacity: 0, scale: 0.5, filter: 'blur(7px)' }}
1019
+ animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
1020
+ exit={{ opacity: 0, scale: 0.5, filter: 'blur(7px)' }}
1021
+ transition={{ duration: 0.15 }}
1022
+ >
1023
+ <Icon />
1024
+ </motion.div>
1025
+ </AnimatePresence>
1026
+ ```
1027
+
1028
+ **Morph surfaces with overflow: hidden:**
1029
+
1030
+ When morphing between shapes, prevent content from spilling:
1031
+
1032
+ ```jsx
1033
+ // Container with overflow: hidden + crossfade content
1034
+ <motion.div
1035
+ layout
1036
+ style={{ overflow: 'hidden', borderRadius: 12 }}
1037
+ transition={{ layout: { duration: 0.3 } }}
1038
+ >
1039
+ <AnimatePresence mode="wait">
1040
+ <motion.div
1041
+ key={contentKey}
1042
+ initial={{ opacity: 0 }}
1043
+ animate={{ opacity: 1 }}
1044
+ exit={{ opacity: 0 }}
1045
+ >
1046
+ {content}
1047
+ </motion.div>
1048
+ </AnimatePresence>
1049
+ </motion.div>
1050
+ ```
1051
+
1052
+ ### High-Frequency Actions
1053
+
1054
+ **No fade-in for menus:**
1055
+
1056
+ Menus triggered frequently should appear instantly, but can fade out:
1057
+
1058
+ ```jsx
1059
+ // Instant appear, animated dismiss
1060
+ <motion.div
1061
+ initial={false} // Skip enter animation
1062
+ animate={{ opacity: 1 }}
1063
+ exit={{ opacity: 0, transition: { duration: 0.1 } }}
1064
+ >
1065
+ <Menu />
1066
+ </motion.div>
1067
+ ```
1068
+
1069
+ **Skip animation between rapid data updates:**
1070
+
1071
+ ```jsx
1072
+ // Skip animation if update is too fast
1073
+ const lastUpdate = useRef(Date.now());
1074
+ const ANIMATION_THRESHOLD = 100; // ms
1075
+
1076
+ function updateValue(newValue) {
1077
+ const now = Date.now();
1078
+ const shouldAnimate = now - lastUpdate.current > ANIMATION_THRESHOLD;
1079
+ lastUpdate.current = now;
1080
+
1081
+ if (shouldAnimate) {
1082
+ animateToValue(newValue);
1083
+ } else {
1084
+ setValueInstantly(newValue);
1085
+ }
1086
+ }
1087
+ ```
1088
+
1089
+ **Keyboard interactions often need no animation:**
1090
+
1091
+ ```jsx
1092
+ // Tab navigation - instant focus, no animation
1093
+ // Arrow key navigation - instant highlight
1094
+ // Enter to select - instant (or very fast 50ms)
1095
+ const keyboardTransition = { duration: 0.05 };
1096
+ const pointerTransition = { duration: 0.15 };
1097
+
1098
+ const transition = isKeyboardNav ? keyboardTransition : pointerTransition;
1099
+ ```
1100
+
1101
+ ### Gesture Lifecycle
1102
+
1103
+ **Three phases of gesture handling:**
1104
+
1105
+ ```
1106
+ START (pointer down) → Set constraints, capture pointer
1107
+ MOVE (pointer move) → Update position continuously
1108
+ END (pointer up) → Animate to final position
1109
+ ```
1110
+
1111
+ **Use jump() for continuous updates, set() for animated snap:**
1112
+
1113
+ ```jsx
1114
+ import { useMotionValue, useSpring } from 'framer-motion';
1115
+
1116
+ function DraggableElement() {
1117
+ const x = useMotionValue(0);
1118
+ const springX = useSpring(x, { stiffness: 300, damping: 30 });
1119
+
1120
+ const handleDrag = (e) => {
1121
+ // During drag: jump() for instant tracking (no spring delay)
1122
+ x.jump(e.clientX - startX);
1123
+ };
1124
+
1125
+ const handleDragEnd = () => {
1126
+ // On release: set() for animated snap to final position
1127
+ springX.set(snapToNearest(x.get()));
1128
+ };
1129
+
1130
+ return <motion.div style={{ x: springX }} />;
1131
+ }
1132
+ ```
1133
+
1134
+ **useTransform for derived values:**
1135
+
1136
+ ```jsx
1137
+ import { useMotionValue, useTransform } from 'framer-motion';
1138
+
1139
+ function SwipeCard() {
1140
+ const x = useMotionValue(0);
1141
+
1142
+ // Derive rotation from horizontal position
1143
+ const rotate = useTransform(x, [-200, 200], [-15, 15]);
1144
+
1145
+ // Derive opacity from position
1146
+ const opacity = useTransform(x, [-200, 0, 200], [0.5, 1, 0.5]);
1147
+
1148
+ return (
1149
+ <motion.div
1150
+ style={{ x, rotate, opacity }}
1151
+ drag="x"
1152
+ dragConstraints={{ left: 0, right: 0 }}
1153
+ />
1154
+ );
1155
+ }
1156
+ ```
1157
+
1158
+ **Gesture state tracking:**
1159
+
1160
+ ```jsx
1161
+ function useGestureState() {
1162
+ const [state, setState] = useState('idle');
1163
+ // idle → press → drag → drag-end → idle
1164
+
1165
+ const handlers = {
1166
+ onPointerDown: () => setState('press'),
1167
+ onDragStart: () => setState('drag'),
1168
+ onDragEnd: () => {
1169
+ setState('drag-end');
1170
+ // Return to idle after animation
1171
+ setTimeout(() => setState('idle'), 300);
1172
+ },
1173
+ onPointerUp: () => {
1174
+ if (state === 'press') setState('idle'); // Was click, not drag
1175
+ }
1176
+ };
1177
+
1178
+ return { state, handlers };
1179
+ }
1180
+ ```
1181
+
1182
+ ### Animation Performance
1183
+
1184
+ **Only animate compositor properties:**
1185
+
1186
+ ```css
1187
+ /* ✅ GPU-accelerated (cheap) */
1188
+ transform: translateX(100px);
1189
+ transform: scale(1.1);
1190
+ transform: rotate(45deg);
1191
+ opacity: 0.5;
1192
+
1193
+ /* ❌ Triggers layout/paint (expensive) */
1194
+ width: 200px;
1195
+ height: 200px;
1196
+ top: 100px;
1197
+ left: 100px;
1198
+ margin: 20px;
1199
+ padding: 20px;
1200
+ ```
1201
+
1202
+ **Never use `transition: all`:**
1203
+
1204
+ ```css
1205
+ /* ❌ Bad - animates everything including layout properties */
1206
+ .element {
1207
+ transition: all 0.3s ease;
1208
+ }
1209
+
1210
+ /* ✅ Good - explicit properties */
1211
+ .element {
1212
+ transition: transform 0.3s var(--ease-out-cubic),
1213
+ opacity 0.3s var(--ease-out-cubic);
1214
+ }
1215
+ ```
1216
+
1217
+ **Fix transform shakiness:**
1218
+
1219
+ ```css
1220
+ /* Add will-change if animation looks shaky */
1221
+ .animated-element {
1222
+ will-change: transform;
1223
+ }
1224
+
1225
+ /* Remove after animation completes to free memory */
1226
+ ```
1227
+
1228
+ **CSS vs JavaScript animations:**
1229
+
1230
+ | Use CSS | Use JavaScript |
1231
+ |---|---|
1232
+ | Simple state transitions | Complex sequences |
1233
+ | Hover/focus effects | Gesture-based |
1234
+ | No user interaction during | Interruptible animations |
1235
+ | Performance-critical | Dynamic values |
1236
+
1237
+ ### Reduced Motion
1238
+
1239
+ **Every animation needs a reduced motion alternative:**
1240
+
1241
+ ```css
1242
+ /* Base animation */
1243
+ .modal {
1244
+ animation: slideIn 0.3s var(--ease-out-cubic);
1245
+ }
1246
+
1247
+ @keyframes slideIn {
1248
+ from {
1249
+ opacity: 0;
1250
+ transform: translateY(16px);
1251
+ }
1252
+ }
1253
+
1254
+ /* Reduced motion: instant or fade only */
1255
+ @media (prefers-reduced-motion: reduce) {
1256
+ .modal {
1257
+ animation: fadeIn 0.15s ease;
1258
+ }
1259
+
1260
+ @keyframes fadeIn {
1261
+ from { opacity: 0; }
1262
+ }
1263
+ }
1264
+ ```
1265
+
1266
+ **Framer Motion hook:**
1267
+
1268
+ ```jsx
1269
+ import { useReducedMotion } from 'framer-motion';
1270
+
1271
+ function Modal({ children }) {
1272
+ const shouldReduceMotion = useReducedMotion();
1273
+
1274
+ return (
1275
+ <motion.div
1276
+ initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 16 }}
1277
+ animate={{ opacity: 1, y: 0 }}
1278
+ transition={{ duration: shouldReduceMotion ? 0.1 : 0.3 }}
1279
+ >
1280
+ {children}
1281
+ </motion.div>
1282
+ );
1283
+ }
1284
+ ```
1285
+
1286
+ **What reduced motion should do:**
1287
+
1288
+ - Remove parallax effects
1289
+ - Stop auto-playing videos/carousels
1290
+ - Replace slide/scale with fade or instant
1291
+ - Keep essential feedback (success checkmarks can still appear, just not animated)
1292
+
1293
+ ---
1294
+
1295
+ ## Part 6: Accessibility Essentials
1296
+
1297
+ ### WCAG Quick Reference
1298
+
1299
+ **Perceivable:**
1300
+
1301
+ - Color contrast: 4.5:1 text, 3:1 UI components
1302
+ - Don't rely on color alone (add icons, patterns)
1303
+ - Text resizable to 200% without loss
1304
+ - Captions for video; transcripts for audio
1305
+
1306
+ **Operable:**
1307
+
1308
+ - All functionality via keyboard
1309
+ - No keyboard traps
1310
+ - Skip links for repeated content
1311
+ - Touch targets: 44x44px minimum
1312
+
1313
+ **Understandable:**
1314
+
1315
+ - Consistent navigation
1316
+ - Identify input errors clearly
1317
+ - Labels and instructions for forms
1318
+
1319
+ **Robust:**
1320
+
1321
+ - Semantic HTML elements
1322
+ - ARIA only when HTML isn't enough
1323
+ - Tested with screen readers
1324
+
1325
+ ### Focus Management
1326
+
1327
+ **Use `:focus-visible` over `:focus`:**
1328
+
1329
+ ```css
1330
+ /* ✅ Only show focus ring for keyboard navigation */
1331
+ :focus-visible {
1332
+ outline: 2px solid var(--color-primary);
1333
+ outline-offset: 2px;
1334
+ }
1335
+
1336
+ /* ❌ Don't remove outline without replacement */
1337
+ :focus {
1338
+ outline: none; /* BAD - removes accessibility */
1339
+ }
1340
+ ```
1341
+
1342
+ **Compound controls:**
1343
+
1344
+ ```css
1345
+ /* Highlight parent when any child is focused */
1346
+ .input-group:focus-within {
1347
+ box-shadow: 0 0 0 2px var(--color-primary);
1348
+ }
1349
+ ```
1350
+
1351
+ ### Keyboard Navigation
1352
+
1353
+ **All interactive elements must be keyboard-operable:**
1354
+
1355
+ ```jsx
1356
+ // ❌ Click-only interaction
1357
+ <div onClick={handleAction}>Click me</div>
1358
+
1359
+ // ✅ Keyboard accessible
1360
+ <button onClick={handleAction}>Click me</button>
1361
+
1362
+ // ✅ If must use div, add keyboard support
1363
+ <div
1364
+ role="button"
1365
+ tabIndex={0}
1366
+ onClick={handleAction}
1367
+ onKeyDown={(e) => {
1368
+ if (e.key === 'Enter' || e.key === ' ') {
1369
+ e.preventDefault();
1370
+ handleAction();
1371
+ }
1372
+ }}
1373
+ >
1374
+ Click me
1375
+ </div>
1376
+ ```
1377
+
1378
+ **Keyboard patterns:**
1379
+
1380
+ - Tab order must match visual order
1381
+ - Enter/Space activate buttons and links
1382
+ - Escape closes dialogs and dropdowns
1383
+ - Arrow keys navigate within components (tabs, menus)
1384
+
1385
+ ### ARIA Patterns
1386
+
1387
+ **Icon buttons require `aria-label`:**
1388
+
1389
+ ```jsx
1390
+ // ❌ No accessible name
1391
+ <button><CloseIcon /></button>
1392
+
1393
+ // ✅ Accessible
1394
+ <button aria-label="Close dialog"><CloseIcon /></button>
1395
+ ```
1396
+
1397
+ **Form controls require labels:**
1398
+
1399
+ ```jsx
1400
+ // ❌ No label
1401
+ <input type="email" placeholder="Email" />
1402
+
1403
+ // ✅ Visible label
1404
+ <label>
1405
+ Email
1406
+ <input type="email" />
1407
+ </label>
1408
+
1409
+ // ✅ Or visually hidden label
1410
+ <label htmlFor="email" className="sr-only">Email</label>
1411
+ <input id="email" type="email" placeholder="email@example.com" />
1412
+ ```
1413
+
1414
+ **Live regions for async updates:**
1415
+
1416
+ ```jsx
1417
+ // Announce dynamic content to screen readers
1418
+ <div aria-live="polite" aria-atomic="true">
1419
+ {statusMessage}
1420
+ </div>
1421
+ ```
1422
+
1423
+ **Semantic HTML before ARIA:**
1424
+
1425
+ ```jsx
1426
+ // ❌ ARIA role when native element exists
1427
+ <div role="button" tabIndex={0}>Submit</div>
1428
+
1429
+ // ✅ Use native element
1430
+ <button>Submit</button>
1431
+
1432
+ // ❌ ARIA for native functionality
1433
+ <div role="navigation">...</div>
1434
+
1435
+ // ✅ Use native element
1436
+ <nav>...</nav>
1437
+ ```
1438
+
1439
+ **Common ARIA patterns:**
1440
+
1441
+ ```html
1442
+ <!-- Modal -->
1443
+ <div role="dialog" aria-modal="true" aria-labelledby="modal-title">
1444
+ <h2 id="modal-title">Dialog Title</h2>
1445
+ </div>
1446
+
1447
+ <!-- Tab panel -->
1448
+ <div role="tablist">
1449
+ <button role="tab" aria-selected="true" aria-controls="panel1">Tab 1</button>
1450
+ </div>
1451
+ <div role="tabpanel" id="panel1">Content</div>
1452
+
1453
+ <!-- Loading state -->
1454
+ <button aria-busy="true" aria-describedby="loading-text">
1455
+ <span id="loading-text" className="sr-only">Loading...</span>
1456
+ </button>
1457
+ ```
1458
+
1459
+ ---
1460
+
1461
+ ## Part 7: Performance Patterns
1462
+
1463
+ ### Virtualization
1464
+
1465
+ **Large lists require virtualization:**
1466
+
1467
+ ```jsx
1468
+ // Use virtualization for lists > 50 items
1469
+ import { VList } from 'virtua';
1470
+
1471
+ function LargeList({ items }) {
1472
+ return (
1473
+ <VList style={{ height: 400 }}>
1474
+ {items.map(item => <ListItem key={item.id} item={item} />)}
1475
+ </VList>
1476
+ );
1477
+ }
1478
+ ```
1479
+
1480
+ **CSS-based virtualization:**
1481
+
1482
+ ```css
1483
+ /* For simpler cases, use content-visibility */
1484
+ .list-item {
1485
+ content-visibility: auto;
1486
+ contain-intrinsic-size: 0 60px; /* Estimated height */
1487
+ }
1488
+ ```
1489
+
1490
+ ### Layout Thrashing
1491
+
1492
+ **Avoid layout reads in render:**
1493
+
1494
+ ```jsx
1495
+ // ❌ Bad - forces layout recalculation
1496
+ function Component() {
1497
+ const width = element.getBoundingClientRect().width; // Layout read
1498
+ element.style.width = width + 10 + 'px'; // Layout write
1499
+ const height = element.offsetHeight; // Another layout read!
1500
+ }
1501
+
1502
+ // ✅ Good - batch reads, then writes
1503
+ function Component() {
1504
+ // Batch reads
1505
+ const width = element.getBoundingClientRect().width;
1506
+ const height = element.offsetHeight;
1507
+
1508
+ // Then batch writes
1509
+ requestAnimationFrame(() => {
1510
+ element.style.width = width + 10 + 'px';
1511
+ element.style.height = height + 10 + 'px';
1512
+ });
1513
+ }
1514
+ ```
1515
+
1516
+ **Properties that trigger layout:**
1517
+
1518
+ - `offsetHeight`, `offsetWidth`, `offsetTop`, `offsetLeft`
1519
+ - `getBoundingClientRect()`
1520
+ - `scrollHeight`, `scrollWidth`, `scrollTop`, `scrollLeft`
1521
+ - `getComputedStyle()`
1522
+
1523
+ ### Resource Loading
1524
+
1525
+ **Preconnect to CDN domains:**
1526
+
1527
+ ```html
1528
+ <!-- Add in <head> for domains you'll fetch from -->
1529
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
1530
+ <link rel="preconnect" href="https://cdn.example.com" crossorigin />
1531
+ ```
1532
+
1533
+ **Preload critical fonts:**
1534
+
1535
+ ```html
1536
+ <link
1537
+ rel="preload"
1538
+ href="/fonts/inter-var.woff2"
1539
+ as="font"
1540
+ type="font/woff2"
1541
+ crossorigin
1542
+ />
1543
+ ```
1544
+
1545
+ **Image loading strategy:**
1546
+
1547
+ ```jsx
1548
+ // Above the fold: load immediately
1549
+ <img src="/hero.jpg" fetchpriority="high" />
1550
+
1551
+ // Below the fold: lazy load
1552
+ <img src="/card.jpg" loading="lazy" />
1553
+
1554
+ // Critical background images: preload
1555
+ <link rel="preload" as="image" href="/hero-bg.jpg" />
1556
+ ```
1557
+
1558
+ ---
1559
+
1560
+ ## Part 8: Content & Copy
1561
+
1562
+ ### Writing Style
1563
+
1564
+ **Active voice over passive:**
1565
+
1566
+ ```
1567
+ ✅ "Install the CLI"
1568
+ ❌ "The CLI will be installed"
1569
+
1570
+ ✅ "Your changes were saved"
1571
+ ❌ "Changes have been saved by the system"
1572
+ ```
1573
+
1574
+ **Title Case for headings and buttons:**
1575
+
1576
+ ```
1577
+ ✅ "Save API Key"
1578
+ ❌ "Save api key"
1579
+
1580
+ ✅ "Getting Started"
1581
+ ❌ "Getting started"
1582
+ ```
1583
+
1584
+ **Use numerals:**
1585
+
1586
+ ```
1587
+ ✅ "8 deployments"
1588
+ ❌ "eight deployments"
1589
+
1590
+ ✅ "3 items selected"
1591
+ ❌ "three items selected"
1592
+ ```
1593
+
1594
+ ### Labels & Messages
1595
+
1596
+ **Specific labels over generic:**
1597
+
1598
+ ```
1599
+ ✅ "Save API Key"
1600
+ ❌ "Continue"
1601
+
1602
+ ✅ "Create Project"
1603
+ ❌ "Submit"
1604
+
1605
+ ✅ "Delete Repository"
1606
+ ❌ "Confirm"
1607
+ ```
1608
+
1609
+ **Error messages include fix/next step:**
1610
+
1611
+ ```
1612
+ ✅ "Email must include @ symbol"
1613
+ ❌ "Invalid email"
1614
+
1615
+ ✅ "Password must be at least 8 characters"
1616
+ ❌ "Password too short"
1617
+
1618
+ ✅ "Could not connect. Check your internet connection and try again."
1619
+ ❌ "Network error"
1620
+ ```
1621
+
1622
+ ### Internationalization
1623
+
1624
+ **Use Intl APIs for formatting:**
1625
+
1626
+ ```jsx
1627
+ // ❌ Hardcoded format
1628
+ const date = `${month}/${day}/${year}`;
1629
+ const price = `$${amount.toFixed(2)}`;
1630
+
1631
+ // ✅ Locale-aware formatting
1632
+ const date = new Intl.DateTimeFormat('en-US', {
1633
+ dateStyle: 'medium'
1634
+ }).format(new Date());
1635
+
1636
+ const price = new Intl.NumberFormat('en-US', {
1637
+ style: 'currency',
1638
+ currency: 'USD'
1639
+ }).format(amount);
1640
+
1641
+ // Relative time
1642
+ const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
1643
+ rtf.format(-1, 'day'); // "yesterday"
1644
+ ```
1645
+
1646
+ ---
1647
+
1648
+ ## Part 9: Anti-Patterns Checklist
1649
+
1650
+ Flag these patterns during design review:
1651
+
1652
+ ### Accessibility Violations
1653
+
1654
+ - [ ] `user-scalable=no` or `maximum-scale=1` in viewport meta
1655
+ - [ ] `<div onClick>` instead of `<button>` for interactive elements
1656
+ - [ ] Form inputs without associated labels
1657
+ - [ ] Icon buttons without `aria-label`
1658
+ - [ ] `outline: none` without focus replacement
1659
+
1660
+ ### Performance Issues
1661
+
1662
+ - [ ] `transition: all` (animates layout properties)
1663
+ - [ ] Images without explicit `width` and `height`
1664
+ - [ ] Large arrays (>50 items) rendered without virtualization
1665
+ - [ ] Layout reads (`getBoundingClientRect`) in render cycle
1666
+
1667
+ ### UX Problems
1668
+
1669
+ - [ ] `onPaste` with `preventDefault()` (blocks paste)
1670
+ - [ ] Hardcoded date/number formats (not using Intl)
1671
+ - [ ] Placeholder used as label
1672
+ - [ ] Validation on every keystroke
1673
+ - [ ] No empty state designed
1674
+
1675
+ ### Mobile Issues
1676
+
1677
+ - [ ] Touch targets smaller than 44x44px
1678
+ - [ ] No `touch-action: manipulation` on buttons
1679
+ - [ ] Hover effects without `@media (hover: hover)` query
1680
+
1681
+ ### Interface Robustness Checklist
1682
+
1683
+ Test every interactive component against these stress conditions:
1684
+
1685
+ **Rapid User Input:**
1686
+ - [ ] Scroll fast — does it break or show visual glitches?
1687
+ - [ ] Spam click — does it trigger multiple actions or crash?
1688
+ - [ ] Resize window rapidly — does layout break or cause errors?
1689
+ - [ ] Type very fast in inputs — does it lag or lose characters?
1690
+
1691
+ **Animation Interruption:**
1692
+ - [ ] Interrupt animations mid-way — does it recover gracefully?
1693
+ - [ ] Close modal while opening — does it handle state correctly?
1694
+ - [ ] Navigate away during transition — does it clean up properly?
1695
+ - [ ] Trigger multiple animations simultaneously — do they conflict?
1696
+
1697
+ **Network Conditions:**
1698
+ - [ ] Test with slow network (3G throttling) — does it show loading states?
1699
+ - [ ] Test with offline mode — does it fail gracefully?
1700
+ - [ ] Test with request timeout — does it retry or show error?
1701
+
1702
+ **Input Methods:**
1703
+ - [ ] Test with keyboard only — is everything accessible?
1704
+ - [ ] Test with screen reader — are states announced?
1705
+ - [ ] Test with touch device — are hit areas adequate?
1706
+ - [ ] Test with trackpad gestures — do scroll/swipe work correctly?
1707
+
1708
+ **State Combinations:**
1709
+ - [ ] Test all combinations of loading + error + empty states
1710
+ - [ ] Test disabled state during async operations
1711
+ - [ ] Test hover + focus + active simultaneously
1712
+ - [ ] Test with maximum and minimum content
1713
+
1714
+ **Memory & Performance:**
1715
+ - [ ] Run for extended period — does memory grow unbounded?
1716
+ - [ ] Test with large datasets — does it virtualize or paginate?
1717
+ - [ ] Monitor for memory leaks in animations and observers
1718
+ - [ ] Check for event listener cleanup on unmount
1719
+
1720
+ ---
1721
+
1722
+ ## Part 10: Design System References
1723
+
1724
+ ### Study These Systems
1725
+
1726
+ **For Clarity & Precision:**
1727
+
1728
+ - [Linear](https://linear.app) - Information density done right
1729
+ - [Stripe](https://stripe.com) - Trust through craft
1730
+ - [Vercel](https://vercel.com) - Developer-focused simplicity
1731
+
1732
+ **For Warmth & Approachability:**
1733
+
1734
+ - [Airbnb](https://airbnb.com) - Friendly, image-forward
1735
+ - [Notion](https://notion.so) - Flexible, playful
1736
+ - [Slack](https://slack.com) - Conversational, colorful
1737
+
1738
+ **For Data & Density:**
1739
+
1740
+ - [Bloomberg Terminal](https://bloomberg.com) - Maximum information
1741
+ - [Figma](https://figma.com) - Tool-like precision
1742
+ - [GitHub](https://github.com) - Code-centric clarity
1743
+
1744
+ **For Motion & Delight:**
1745
+
1746
+ - [Apple](https://apple.com) - Cinematic quality
1747
+ - [Framer](https://framer.com) - Motion-first
1748
+
1749
+ ### When Generating Variants
1750
+
1751
+ Reference specific aspects:
1752
+
1753
+ - "Use Linear's density approach"
1754
+ - "Stripe's button hierarchy"
1755
+ - "Airbnb's card layout"
1756
+ - "Notion's toggle interaction"
1757
+ - "Vercel's dark mode palette"
1758
+
1759
+ ---
1760
+
1761
+ ## Part 11: Code Patterns Library
1762
+
1763
+ Practical patterns for implementing advanced interactions.
1764
+
1765
+ ### useMotionValue vs useState
1766
+
1767
+ **Performance difference:**
1768
+
1769
+ ```jsx
1770
+ // ❌ Bad - triggers re-render on every frame
1771
+ const [x, setX] = useState(0);
1772
+ <div style={{ transform: `translateX(${x}px)` }} />
1773
+
1774
+ // ✅ Good - bypasses React, updates directly
1775
+ const x = useMotionValue(0);
1776
+ <motion.div style={{ x }} />
1777
+ ```
1778
+
1779
+ Use `useMotionValue` for:
1780
+ - Continuous animation values (position, scale, opacity)
1781
+ - Gesture-driven values (drag position)
1782
+ - Any value that changes at 60fps
1783
+
1784
+ Use `useState` for:
1785
+ - Discrete states (open/closed, active tab)
1786
+ - Values that trigger layout changes
1787
+
1788
+ ### useSpring: jump() vs set()
1789
+
1790
+ ```jsx
1791
+ const springValue = useSpring(0, { stiffness: 300, damping: 30 });
1792
+
1793
+ // set() - Animate to target value with spring physics
1794
+ springValue.set(100); // Smoothly animates from current → 100
1795
+
1796
+ // jump() - Instantly set value, no animation
1797
+ springValue.jump(100); // Immediately becomes 100
1798
+
1799
+ // Use jump() during drag, set() on release
1800
+ const handleDrag = (e) => springValue.jump(e.clientX);
1801
+ const handleRelease = () => springValue.set(snapPoint);
1802
+ ```
1803
+
1804
+ ### Grid Stacking for Overlapping Elements
1805
+
1806
+ **Stack elements using CSS Grid (no absolute positioning):**
1807
+
1808
+ ```jsx
1809
+ // All children occupy the same grid cell
1810
+ function StackedElements({ children }) {
1811
+ return (
1812
+ <div style={{
1813
+ display: 'grid',
1814
+ gridTemplateColumns: '1fr',
1815
+ gridTemplateRows: '1fr',
1816
+ }}>
1817
+ {children.map((child, i) => (
1818
+ <div key={i} style={{ gridArea: '1 / 1' }}>
1819
+ {child}
1820
+ </div>
1821
+ ))}
1822
+ </div>
1823
+ );
1824
+ }
1825
+ ```
1826
+
1827
+ **Benefits over `position: absolute`:**
1828
+ - Children contribute to parent size
1829
+ - Natural document flow
1830
+ - Better for responsive layouts
1831
+
1832
+ ### Native Scroll vs Wheel Events
1833
+
1834
+ **Prefer native scroll over wheel event listeners:**
1835
+
1836
+ ```jsx
1837
+ // ❌ Bad - janky, blocks main thread
1838
+ element.addEventListener('wheel', (e) => {
1839
+ scrollPosition += e.deltaY;
1840
+ element.style.transform = `translateY(${-scrollPosition}px)`;
1841
+ });
1842
+
1843
+ // ✅ Good - smooth, GPU-accelerated
1844
+ <div style={{ overflow: 'auto', scrollBehavior: 'smooth' }}>
1845
+ {content}
1846
+ </div>
1847
+ ```
1848
+
1849
+ **When you need scroll position reactively:**
1850
+
1851
+ ```jsx
1852
+ // Use Intersection Observer for scroll-triggered effects
1853
+ const observer = new IntersectionObserver(
1854
+ (entries) => {
1855
+ entries.forEach(entry => {
1856
+ if (entry.isIntersecting) {
1857
+ entry.target.classList.add('visible');
1858
+ }
1859
+ });
1860
+ },
1861
+ { threshold: 0.1 }
1862
+ );
1863
+ ```
1864
+
1865
+ ### clip-path for Animated Resizing
1866
+
1867
+ **Animate size without layout shifts:**
1868
+
1869
+ ```jsx
1870
+ // ❌ Bad - animating width triggers layout
1871
+ <motion.div animate={{ width: isOpen ? 300 : 0 }} />
1872
+
1873
+ // ✅ Good - clip-path is GPU-accelerated
1874
+ <motion.div
1875
+ style={{ width: 300 }} // Fixed width
1876
+ animate={{
1877
+ clipPath: isOpen
1878
+ ? 'inset(0 0 0 0)'
1879
+ : 'inset(0 100% 0 0)' // Clip from right
1880
+ }}
1881
+ />
1882
+ ```
1883
+
1884
+ **Reveal patterns:**
1885
+
1886
+ ```css
1887
+ /* Reveal from left */
1888
+ clip-path: inset(0 100% 0 0) → inset(0 0 0 0)
1889
+
1890
+ /* Reveal from center */
1891
+ clip-path: inset(0 50% 0 50%) → inset(0 0 0 0)
1892
+
1893
+ /* Reveal from top */
1894
+ clip-path: inset(0 0 100% 0) → inset(0 0 0 0)
1895
+
1896
+ /* Circle reveal from center */
1897
+ clip-path: circle(0% at 50% 50%) → circle(100% at 50% 50%)
1898
+ ```
1899
+
1900
+ ### Scroll Fading (Blur Fade Effect)
1901
+
1902
+ **Fade content at scroll edges:**
1903
+
1904
+ ```css
1905
+ .scroll-container {
1906
+ overflow-y: auto;
1907
+ mask-image: linear-gradient(
1908
+ to bottom,
1909
+ transparent 0%,
1910
+ black 10%,
1911
+ black 90%,
1912
+ transparent 100%
1913
+ );
1914
+ }
1915
+
1916
+ /* Or use scroll-driven animations (modern browsers) */
1917
+ @supports (animation-timeline: scroll()) {
1918
+ .scroll-item {
1919
+ animation: fadeIn linear both;
1920
+ animation-timeline: view();
1921
+ animation-range: entry 0% entry 20%;
1922
+ }
1923
+
1924
+ @keyframes fadeIn {
1925
+ from { opacity: 0; transform: translateY(20px); }
1926
+ to { opacity: 1; transform: translateY(0); }
1927
+ }
1928
+ }
1929
+ ```
1930
+
1931
+ ### overflow: clip vs hidden
1932
+
1933
+ **Use `clip` when you don't need scroll:**
1934
+
1935
+ ```css
1936
+ /* hidden: creates scroll container, may affect stacking */
1937
+ .container { overflow: hidden; }
1938
+
1939
+ /* clip: just clips, no scroll container created */
1940
+ .container { overflow: clip; }
1941
+ ```
1942
+
1943
+ **When to use each:**
1944
+ - `hidden` - Need scrollable overflow (JS can scroll it)
1945
+ - `clip` - Just want to hide overflow, no scroll needed (better perf)
1946
+
1947
+ ### Re-mount with React Key for CSS Keyframes
1948
+
1949
+ **Replay CSS animations by changing key:**
1950
+
1951
+ ```jsx
1952
+ // CSS animation plays once on mount
1953
+ function Notification({ message }) {
1954
+ const [key, setKey] = useState(0);
1955
+
1956
+ const triggerAnimation = () => setKey(k => k + 1);
1957
+
1958
+ return (
1959
+ <div key={key} className="animate-shake">
1960
+ {message}
1961
+ </div>
1962
+ );
1963
+ }
1964
+ ```
1965
+
1966
+ ```css
1967
+ .animate-shake {
1968
+ animation: shake 0.5s ease-out;
1969
+ }
1970
+
1971
+ @keyframes shake {
1972
+ 0%, 100% { transform: translateX(0); }
1973
+ 25% { transform: translateX(-5px); }
1974
+ 75% { transform: translateX(5px); }
1975
+ }
1976
+ ```
1977
+
1978
+ ### Smart Image Preloading
1979
+
1980
+ **Preload images before they're needed:**
1981
+
1982
+ ```jsx
1983
+ // Preload on hover (link navigation)
1984
+ function NavLink({ href, children }) {
1985
+ const preloadImages = () => {
1986
+ const images = ['/hero.jpg', '/feature.png'];
1987
+ images.forEach(src => {
1988
+ const img = new Image();
1989
+ img.src = src;
1990
+ });
1991
+ };
1992
+
1993
+ return (
1994
+ <Link
1995
+ href={href}
1996
+ onMouseEnter={preloadImages}
1997
+ onFocus={preloadImages}
1998
+ >
1999
+ {children}
2000
+ </Link>
2001
+ );
2002
+ }
2003
+
2004
+ // Preload based on viewport proximity
2005
+ function useImagePreload(src, rootMargin = '200px') {
2006
+ const ref = useRef(null);
2007
+
2008
+ useEffect(() => {
2009
+ const observer = new IntersectionObserver(
2010
+ ([entry]) => {
2011
+ if (entry.isIntersecting) {
2012
+ const img = new Image();
2013
+ img.src = src;
2014
+ observer.disconnect();
2015
+ }
2016
+ },
2017
+ { rootMargin }
2018
+ );
2019
+
2020
+ if (ref.current) observer.observe(ref.current);
2021
+ return () => observer.disconnect();
2022
+ }, [src, rootMargin]);
2023
+
2024
+ return ref;
2025
+ }
2026
+ ```
2027
+
2028
+ ### layoutId for Shared Element Transitions
2029
+
2030
+ **Morph elements between views:**
2031
+
2032
+ ```jsx
2033
+ import { motion, AnimatePresence } from 'framer-motion';
2034
+
2035
+ function App() {
2036
+ const [selected, setSelected] = useState(null);
2037
+
2038
+ return (
2039
+ <>
2040
+ {/* Grid of cards */}
2041
+ {items.map(item => (
2042
+ <motion.div
2043
+ key={item.id}
2044
+ layoutId={`card-${item.id}`}
2045
+ onClick={() => setSelected(item)}
2046
+ >
2047
+ <Card item={item} />
2048
+ </motion.div>
2049
+ ))}
2050
+
2051
+ {/* Expanded view */}
2052
+ <AnimatePresence>
2053
+ {selected && (
2054
+ <motion.div
2055
+ layoutId={`card-${selected.id}`}
2056
+ className="expanded-card"
2057
+ >
2058
+ <ExpandedCard item={selected} />
2059
+ </motion.div>
2060
+ )}
2061
+ </AnimatePresence>
2062
+ </>
2063
+ );
2064
+ }
2065
+ ```
2066
+
2067
+ ### Debounced Resize Observer
2068
+
2069
+ **Prevent excessive callbacks during resize:**
2070
+
2071
+ ```jsx
2072
+ function useResizeObserver(ref, callback, debounceMs = 100) {
2073
+ useEffect(() => {
2074
+ const element = ref.current;
2075
+ if (!element) return;
2076
+
2077
+ let timeoutId;
2078
+ const observer = new ResizeObserver((entries) => {
2079
+ clearTimeout(timeoutId);
2080
+ timeoutId = setTimeout(() => {
2081
+ callback(entries[0].contentRect);
2082
+ }, debounceMs);
2083
+ });
2084
+
2085
+ observer.observe(element);
2086
+ return () => {
2087
+ clearTimeout(timeoutId);
2088
+ observer.disconnect();
2089
+ };
2090
+ }, [ref, callback, debounceMs]);
2091
+ }
2092
+ ```
2093
+
2094
+ ---
2095
+
2096
+ ## Quick Decision Framework
2097
+
2098
+ When unsure, ask:
2099
+
2100
+ 1. **Is it clear?** → User knows what to do and what happened
2101
+ 2. **Is it fast?** → Minimum steps, appropriate feedback
2102
+ 3. **Is it consistent?** → Matches patterns elsewhere in the app
2103
+ 4. **Is it accessible?** → Keyboard, screen reader, color contrast
2104
+ 5. **Is it calm?** → No unnecessary motion, color, or elements
2105
+ 6. **Is it specific?** → Labels describe exactly what will happen
2106
+ 7. **Is it recoverable?** → User can undo or go back
2107
+
2108
+ ### Animation Decision Quick Check
2109
+
2110
+ ```
2111
+ Should this animate?
2112
+ ├── Used 100+ times/day? → No animation
2113
+ ├── Entering/exiting screen? → ease-out, 150-250ms
2114
+ ├── Moving on screen? → ease-in-out, 200-300ms
2115
+ ├── Hover/color change? → ease, 100-150ms
2116
+ └── Unsure? → Start without animation, add if needed
2117
+ ```
2118
+
2119
+ ### Interaction Pattern Quick Reference
2120
+
2121
+ | Situation | Solution |
2122
+ |-----------|----------|
2123
+ | Overlapping motion | Add 1-2px blur during transition |
2124
+ | Menu animation | Fade-out only, no fade-in (instant appear) |
2125
+ | Keyboard interactions | Often no animation needed (50ms max) |
2126
+ | High-frequency updates | Skip animation if update < 100ms apart |
2127
+ | Drag gesture | Use `jump()` during, `set()` at end |
2128
+ | Touch gestures | Use `touch-action: none` to capture |
2129
+ | Thin hit areas | Expand with `::after` pseudo-element |
2130
+ | Scroll-based animation | Use native scroll, not wheel event |
2131
+ | Width animation | Use `clip-path` instead of width |
2132
+ | Replay CSS animation | Change React `key` prop |
2133
+ | Morph between elements | Use Framer Motion `layoutId` |
2134
+ | State during gesture | Track: idle → press → drag → drag-end |
2135
+ | Overflow without scroll | Use `overflow: clip` |
2136
+ | Exit animations | Double the spring stiffness |
2137
+ | Icon swap | Crossfade with scale 0.5 + blur 7px |
2138
+ | Preload data | Start on `mousedown` (before click) |
2139
+ | Spring animation values | Never reuse — tune per interaction |
2140
+ | React animation perf | Use `useMotionValue`, not `useState` |