niahere 0.2.61 → 0.2.63

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 (186) hide show
  1. package/defaults/memory-promoter.md +99 -0
  2. package/defaults/self/staging.md +48 -0
  3. package/package.json +7 -3
  4. package/skills/code-review/pr-review.md +14 -1
  5. package/skills/cro/page.md +22 -0
  6. package/skills/frontend-design/SKILL.md +7 -3
  7. package/skills/frontend-design/building.md +17 -2
  8. package/skills/qa/SKILL.md +108 -72
  9. package/skills/userinterface-wiki/AGENTS.md +3749 -0
  10. package/skills/userinterface-wiki/SKILL.md +253 -0
  11. package/skills/userinterface-wiki/metadata.json +26 -0
  12. package/skills/userinterface-wiki/rules/_sections.md +66 -0
  13. package/skills/userinterface-wiki/rules/_template.md +24 -0
  14. package/skills/userinterface-wiki/rules/a11y-reduced-motion-check.md +30 -0
  15. package/skills/userinterface-wiki/rules/a11y-toggle-setting.md +30 -0
  16. package/skills/userinterface-wiki/rules/a11y-visual-equivalent.md +36 -0
  17. package/skills/userinterface-wiki/rules/a11y-volume-control.md +28 -0
  18. package/skills/userinterface-wiki/rules/appropriate-confirmations-only.md +19 -0
  19. package/skills/userinterface-wiki/rules/appropriate-errors-warnings.md +18 -0
  20. package/skills/userinterface-wiki/rules/appropriate-no-decorative.md +21 -0
  21. package/skills/userinterface-wiki/rules/appropriate-no-high-frequency.md +28 -0
  22. package/skills/userinterface-wiki/rules/appropriate-no-punishing.md +27 -0
  23. package/skills/userinterface-wiki/rules/container-callback-ref.md +31 -0
  24. package/skills/userinterface-wiki/rules/container-guard-initial-zero.md +25 -0
  25. package/skills/userinterface-wiki/rules/container-no-excessive-use.md +13 -0
  26. package/skills/userinterface-wiki/rules/container-overflow-hidden.md +25 -0
  27. package/skills/userinterface-wiki/rules/container-transition-delay.md +21 -0
  28. package/skills/userinterface-wiki/rules/container-two-div-pattern.md +35 -0
  29. package/skills/userinterface-wiki/rules/container-use-resize-observer.md +48 -0
  30. package/skills/userinterface-wiki/rules/context-cleanup-nodes.md +25 -0
  31. package/skills/userinterface-wiki/rules/context-resume-suspended.md +28 -0
  32. package/skills/userinterface-wiki/rules/context-reuse-single.md +30 -0
  33. package/skills/userinterface-wiki/rules/design-filter-for-character.md +25 -0
  34. package/skills/userinterface-wiki/rules/design-noise-for-percussion.md +26 -0
  35. package/skills/userinterface-wiki/rules/design-oscillator-for-tonal.md +22 -0
  36. package/skills/userinterface-wiki/rules/duration-max-300ms.md +21 -0
  37. package/skills/userinterface-wiki/rules/duration-press-hover.md +21 -0
  38. package/skills/userinterface-wiki/rules/duration-shorten-before-curve.md +21 -0
  39. package/skills/userinterface-wiki/rules/duration-small-state.md +15 -0
  40. package/skills/userinterface-wiki/rules/easing-entrance-ease-out.md +21 -0
  41. package/skills/userinterface-wiki/rules/easing-exit-ease-in.md +21 -0
  42. package/skills/userinterface-wiki/rules/easing-for-state-change.md +27 -0
  43. package/skills/userinterface-wiki/rules/easing-linear-only-progress.md +21 -0
  44. package/skills/userinterface-wiki/rules/easing-natural-decay.md +22 -0
  45. package/skills/userinterface-wiki/rules/easing-no-linear-motion.md +22 -0
  46. package/skills/userinterface-wiki/rules/easing-transition-ease-in-out.md +15 -0
  47. package/skills/userinterface-wiki/rules/envelope-exponential-decay.md +21 -0
  48. package/skills/userinterface-wiki/rules/envelope-no-zero-target.md +21 -0
  49. package/skills/userinterface-wiki/rules/envelope-set-initial-value.md +22 -0
  50. package/skills/userinterface-wiki/rules/exit-key-required.md +29 -0
  51. package/skills/userinterface-wiki/rules/exit-matches-initial.md +29 -0
  52. package/skills/userinterface-wiki/rules/exit-prop-required.md +33 -0
  53. package/skills/userinterface-wiki/rules/exit-requires-wrapper.md +27 -0
  54. package/skills/userinterface-wiki/rules/impl-default-subtle.md +21 -0
  55. package/skills/userinterface-wiki/rules/impl-preload-audio.md +34 -0
  56. package/skills/userinterface-wiki/rules/impl-reset-current-time.md +26 -0
  57. package/skills/userinterface-wiki/rules/mode-pop-layout-for-lists.md +25 -0
  58. package/skills/userinterface-wiki/rules/mode-sync-layout-conflict.md +29 -0
  59. package/skills/userinterface-wiki/rules/mode-wait-doubles-duration.md +25 -0
  60. package/skills/userinterface-wiki/rules/morphing-aria-hidden.md +21 -0
  61. package/skills/userinterface-wiki/rules/morphing-consistent-viewbox.md +23 -0
  62. package/skills/userinterface-wiki/rules/morphing-group-variants.md +33 -0
  63. package/skills/userinterface-wiki/rules/morphing-jump-non-grouped.md +29 -0
  64. package/skills/userinterface-wiki/rules/morphing-reduced-motion.md +28 -0
  65. package/skills/userinterface-wiki/rules/morphing-spring-rotation.md +23 -0
  66. package/skills/userinterface-wiki/rules/morphing-strokelinecap-round.md +21 -0
  67. package/skills/userinterface-wiki/rules/morphing-three-lines.md +32 -0
  68. package/skills/userinterface-wiki/rules/morphing-use-collapsed.md +33 -0
  69. package/skills/userinterface-wiki/rules/native-backdrop-styling.md +27 -0
  70. package/skills/userinterface-wiki/rules/native-placeholder-styling.md +27 -0
  71. package/skills/userinterface-wiki/rules/native-selection-styling.md +18 -0
  72. package/skills/userinterface-wiki/rules/nested-consistent-timing.md +25 -0
  73. package/skills/userinterface-wiki/rules/nested-propagate-required.md +41 -0
  74. package/skills/userinterface-wiki/rules/none-context-menu-entrance.md +25 -0
  75. package/skills/userinterface-wiki/rules/none-high-frequency.md +29 -0
  76. package/skills/userinterface-wiki/rules/none-keyboard-navigation.md +32 -0
  77. package/skills/userinterface-wiki/rules/param-click-duration.md +21 -0
  78. package/skills/userinterface-wiki/rules/param-filter-frequency-range.md +21 -0
  79. package/skills/userinterface-wiki/rules/param-q-value-range.md +21 -0
  80. package/skills/userinterface-wiki/rules/param-reasonable-gain.md +21 -0
  81. package/skills/userinterface-wiki/rules/physics-active-state.md +23 -0
  82. package/skills/userinterface-wiki/rules/physics-no-excessive-stagger.md +22 -0
  83. package/skills/userinterface-wiki/rules/physics-spring-for-overshoot.md +23 -0
  84. package/skills/userinterface-wiki/rules/physics-subtle-deformation.md +22 -0
  85. package/skills/userinterface-wiki/rules/prefetch-hit-slop.md +27 -0
  86. package/skills/userinterface-wiki/rules/prefetch-keyboard-tab.md +19 -0
  87. package/skills/userinterface-wiki/rules/prefetch-not-everything.md +22 -0
  88. package/skills/userinterface-wiki/rules/prefetch-touch-fallback.md +34 -0
  89. package/skills/userinterface-wiki/rules/prefetch-trajectory-over-hover.md +32 -0
  90. package/skills/userinterface-wiki/rules/prefetch-use-selectively.md +13 -0
  91. package/skills/userinterface-wiki/rules/presence-disable-interactions.md +31 -0
  92. package/skills/userinterface-wiki/rules/presence-hook-in-child.md +31 -0
  93. package/skills/userinterface-wiki/rules/presence-safe-to-remove.md +37 -0
  94. package/skills/userinterface-wiki/rules/pseudo-content-required.md +28 -0
  95. package/skills/userinterface-wiki/rules/pseudo-first-line-styling.md +27 -0
  96. package/skills/userinterface-wiki/rules/pseudo-hit-target-expansion.md +31 -0
  97. package/skills/userinterface-wiki/rules/pseudo-marker-styling.md +28 -0
  98. package/skills/userinterface-wiki/rules/pseudo-over-dom-node.md +32 -0
  99. package/skills/userinterface-wiki/rules/pseudo-position-relative-parent.md +33 -0
  100. package/skills/userinterface-wiki/rules/pseudo-z-index-layering.md +37 -0
  101. package/skills/userinterface-wiki/rules/spring-for-gestures.md +27 -0
  102. package/skills/userinterface-wiki/rules/spring-for-interruptible.md +27 -0
  103. package/skills/userinterface-wiki/rules/spring-params-balanced.md +29 -0
  104. package/skills/userinterface-wiki/rules/spring-preserves-velocity.md +28 -0
  105. package/skills/userinterface-wiki/rules/staging-dim-background.md +22 -0
  106. package/skills/userinterface-wiki/rules/staging-one-focal-point.md +24 -0
  107. package/skills/userinterface-wiki/rules/staging-z-index-hierarchy.md +22 -0
  108. package/skills/userinterface-wiki/rules/timing-consistent.md +24 -0
  109. package/skills/userinterface-wiki/rules/timing-no-entrance-context-menu.md +22 -0
  110. package/skills/userinterface-wiki/rules/timing-under-300ms.md +22 -0
  111. package/skills/userinterface-wiki/rules/transition-name-cleanup.md +28 -0
  112. package/skills/userinterface-wiki/rules/transition-name-required.md +27 -0
  113. package/skills/userinterface-wiki/rules/transition-name-unique.md +24 -0
  114. package/skills/userinterface-wiki/rules/transition-over-js-library.md +32 -0
  115. package/skills/userinterface-wiki/rules/transition-style-pseudo-elements.md +24 -0
  116. package/skills/userinterface-wiki/rules/type-antialiased-on-retina.md +18 -0
  117. package/skills/userinterface-wiki/rules/type-disambiguation-stylistic-set.md +15 -0
  118. package/skills/userinterface-wiki/rules/type-font-display-swap.md +28 -0
  119. package/skills/userinterface-wiki/rules/type-justify-with-hyphens.md +24 -0
  120. package/skills/userinterface-wiki/rules/type-letter-spacing-uppercase.md +28 -0
  121. package/skills/userinterface-wiki/rules/type-no-font-synthesis.md +18 -0
  122. package/skills/userinterface-wiki/rules/type-oldstyle-nums-for-prose.md +21 -0
  123. package/skills/userinterface-wiki/rules/type-opentype-contextual-alternates.md +15 -0
  124. package/skills/userinterface-wiki/rules/type-optical-sizing-auto.md +25 -0
  125. package/skills/userinterface-wiki/rules/type-proper-fractions.md +15 -0
  126. package/skills/userinterface-wiki/rules/type-slashed-zero.md +17 -0
  127. package/skills/userinterface-wiki/rules/type-tabular-nums-for-data.md +21 -0
  128. package/skills/userinterface-wiki/rules/type-text-wrap-balance-headings.md +21 -0
  129. package/skills/userinterface-wiki/rules/type-text-wrap-pretty.md +16 -0
  130. package/skills/userinterface-wiki/rules/type-underline-offset.md +25 -0
  131. package/skills/userinterface-wiki/rules/type-variable-weight-continuous.md +23 -0
  132. package/skills/userinterface-wiki/rules/ux-aesthetic-usability.md +32 -0
  133. package/skills/userinterface-wiki/rules/ux-cognitive-load-reduce.md +49 -0
  134. package/skills/userinterface-wiki/rules/ux-common-region-boundaries.md +50 -0
  135. package/skills/userinterface-wiki/rules/ux-doherty-perceived-speed.md +29 -0
  136. package/skills/userinterface-wiki/rules/ux-doherty-under-400ms.md +30 -0
  137. package/skills/userinterface-wiki/rules/ux-fitts-hit-area.md +32 -0
  138. package/skills/userinterface-wiki/rules/ux-fitts-target-size.md +31 -0
  139. package/skills/userinterface-wiki/rules/ux-goal-gradient-progress.md +33 -0
  140. package/skills/userinterface-wiki/rules/ux-hicks-minimize-choices.md +45 -0
  141. package/skills/userinterface-wiki/rules/ux-jakobs-familiar-patterns.md +37 -0
  142. package/skills/userinterface-wiki/rules/ux-millers-chunking.md +23 -0
  143. package/skills/userinterface-wiki/rules/ux-pareto-prioritize-features.md +36 -0
  144. package/skills/userinterface-wiki/rules/ux-peak-end-finish-strong.md +35 -0
  145. package/skills/userinterface-wiki/rules/ux-postels-accept-messy-input.md +45 -0
  146. package/skills/userinterface-wiki/rules/ux-pragnanz-simplify.md +33 -0
  147. package/skills/userinterface-wiki/rules/ux-progressive-disclosure.md +41 -0
  148. package/skills/userinterface-wiki/rules/ux-proximity-grouping.md +38 -0
  149. package/skills/userinterface-wiki/rules/ux-serial-position.md +31 -0
  150. package/skills/userinterface-wiki/rules/ux-similarity-consistency.md +35 -0
  151. package/skills/userinterface-wiki/rules/ux-teslers-complexity.md +28 -0
  152. package/skills/userinterface-wiki/rules/ux-uniform-connectedness.md +43 -0
  153. package/skills/userinterface-wiki/rules/ux-von-restorff-emphasis.md +29 -0
  154. package/skills/userinterface-wiki/rules/ux-zeigarnik-show-incomplete.md +36 -0
  155. package/skills/userinterface-wiki/rules/visual-animate-shadow-pseudo.md +49 -0
  156. package/skills/userinterface-wiki/rules/visual-border-alpha-colors.md +25 -0
  157. package/skills/userinterface-wiki/rules/visual-button-shadow-anatomy.md +49 -0
  158. package/skills/userinterface-wiki/rules/visual-concentric-radius.md +40 -0
  159. package/skills/userinterface-wiki/rules/visual-consistent-spacing-scale.md +35 -0
  160. package/skills/userinterface-wiki/rules/visual-layered-shadows.md +30 -0
  161. package/skills/userinterface-wiki/rules/visual-no-pure-black-shadow.md +25 -0
  162. package/skills/userinterface-wiki/rules/visual-shadow-direction.md +25 -0
  163. package/skills/userinterface-wiki/rules/visual-shadow-matches-elevation.md +23 -0
  164. package/skills/userinterface-wiki/rules/weight-duration-matches-action.md +29 -0
  165. package/skills/userinterface-wiki/rules/weight-match-action.md +32 -0
  166. package/src/cli/index.ts +23 -73
  167. package/src/cli/job.ts +25 -92
  168. package/src/cli/status.ts +17 -9
  169. package/src/commands/init.ts +1 -0
  170. package/src/commands/validate.ts +12 -10
  171. package/src/core/agents.ts +6 -19
  172. package/src/core/consolidator.ts +97 -91
  173. package/src/core/daemon.ts +71 -43
  174. package/src/core/finalizer.ts +31 -3
  175. package/src/core/health.ts +5 -17
  176. package/src/core/runner.ts +8 -44
  177. package/src/core/scheduler.ts +12 -49
  178. package/src/core/skills.ts +4 -11
  179. package/src/core/summarizer.ts +7 -21
  180. package/src/db/connection.ts +0 -11
  181. package/src/db/models/job.ts +23 -22
  182. package/src/db/with-db.ts +11 -0
  183. package/src/mcp/server.ts +1 -1
  184. package/src/prompts/environment.md +44 -41
  185. package/src/utils/pid.ts +44 -0
  186. package/src/utils/schedule.ts +39 -0
@@ -0,0 +1,3749 @@
1
+ # User Interface Wiki
2
+
3
+ **Version 3.0.0**
4
+ raphael-salaja
5
+ March 2026
6
+
7
+ > **Note:**
8
+ > This document is mainly for agents and LLMs to follow when reviewing,
9
+ > generating, or refactoring UI code. Humans may also find it useful, but
10
+ > guidance here is optimized for automation and consistency by AI-assisted workflows.
11
+
12
+ ---
13
+
14
+ ## Abstract
15
+
16
+ Comprehensive UI/UX best practices guide for web interfaces, designed for AI agents and LLMs. Contains 152 rules across 12 categories, prioritized by impact from critical (animation principles, timing functions) to incremental (morphing icons, typography). Each rule includes detailed explanations and code examples comparing incorrect vs. correct implementations.
17
+
18
+ ---
19
+
20
+ ## Table of Contents
21
+
22
+ 1. [Animation Principles](#1-animation-principles) — **CRITICAL**
23
+ - 1.1 [User Animations Under 300ms](#11-user-animations-under-300ms)
24
+ - 1.2 [Consistent Timing for Similar Elements](#12-consistent-timing-for-similar-elements)
25
+ - 1.3 [No Entrance Animation on Context Menus](#13-no-entrance-animation-on-context-menus)
26
+ - 1.4 [Exponential Ramps for Natural Decay](#14-exponential-ramps-for-natural-decay)
27
+ - 1.5 [No Linear Easing for Motion](#15-no-linear-easing-for-motion)
28
+ - 1.6 [Active State Scale Transform](#16-active-state-scale-transform)
29
+ - 1.7 [Subtle Squash and Stretch](#17-subtle-squash-and-stretch)
30
+ - 1.8 [Springs for Overshoot and Settle](#18-springs-for-overshoot-and-settle)
31
+ - 1.9 [Stagger Under 50ms Per Item](#19-stagger-under-50ms-per-item)
32
+ - 1.10 [Single Focal Point](#110-single-focal-point)
33
+ - 1.11 [Dim Background for Focus](#111-dim-background-for-focus)
34
+ - 1.12 [Z-Index Layering for Animated Elements](#112-z-index-layering-for-animated-elements)
35
+ 2. [Timing Functions](#2-timing-functions) — **HIGH**
36
+ - 2.1 [Springs for Gesture-Driven Motion](#21-springs-for-gesture-driven-motion)
37
+ - 2.2 [Springs for Interruptible Motion](#22-springs-for-interruptible-motion)
38
+ - 2.3 [Springs Preserve Input Velocity](#23-springs-preserve-input-velocity)
39
+ - 2.4 [Balanced Spring Parameters](#24-balanced-spring-parameters)
40
+ - 2.5 [Easing for System State Changes](#25-easing-for-system-state-changes)
41
+ - 2.6 [Ease-Out for Entrances](#26-ease-out-for-entrances)
42
+ - 2.7 [Ease-In for Exits](#27-ease-in-for-exits)
43
+ - 2.8 [Ease-In-Out for View Transitions](#28-ease-in-out-for-view-transitions)
44
+ - 2.9 [Linear Easing Only for Progress](#29-linear-easing-only-for-progress)
45
+ - 2.10 [Press and Hover 120-180ms](#210-press-and-hover-120-180ms)
46
+ - 2.11 [Small State Changes 180-260ms](#211-small-state-changes-180-260ms)
47
+ - 2.12 [Max 300ms for User Actions](#212-max-300ms-for-user-actions)
48
+ - 2.13 [Shorten Duration Before Adjusting Curve](#213-shorten-duration-before-adjusting-curve)
49
+ - 2.14 [No Animation for High-Frequency Interactions](#214-no-animation-for-high-frequency-interactions)
50
+ - 2.15 [No Animation for Keyboard Navigation](#215-no-animation-for-keyboard-navigation)
51
+ - 2.16 [No Entrance Animation for Context Menus](#216-no-entrance-animation-for-context-menus)
52
+ 3. [Exit Animations](#3-exit-animations) — **HIGH**
53
+ - 3.1 [AnimatePresence Wrapper Required](#31-animatepresence-wrapper-required)
54
+ - 3.2 [Exit Prop Required Inside AnimatePresence](#32-exit-prop-required-inside-animatepresence)
55
+ - 3.3 [Unique Keys in AnimatePresence Lists](#33-unique-keys-in-animatepresence-lists)
56
+ - 3.4 [Exit Mirrors Initial for Symmetry](#34-exit-mirrors-initial-for-symmetry)
57
+ - 3.5 [useIsPresent in Child Component](#35-useispresent-in-child-component)
58
+ - 3.6 [Call safeToRemove After Async Work](#36-call-safetoremove-after-async-work)
59
+ - 3.7 [Disable Interactions on Exiting Elements](#37-disable-interactions-on-exiting-elements)
60
+ - 3.8 [Mode "wait" Doubles Duration](#38-mode-wait-doubles-duration)
61
+ - 3.9 [Mode "sync" Causes Layout Conflicts](#39-mode-sync-causes-layout-conflicts)
62
+ - 3.10 [popLayout for List Reordering](#310-poplayout-for-list-reordering)
63
+ - 3.11 [Propagate Prop for Nested AnimatePresence](#311-propagate-prop-for-nested-animatepresence)
64
+ - 3.12 [Coordinated Parent-Child Exit Timing](#312-coordinated-parent-child-exit-timing)
65
+ 4. [CSS Pseudo Elements](#4-css-pseudo-elements) — **MEDIUM**
66
+ - 4.1 [Content Property Required for Pseudo-Elements](#41-content-property-required-for-pseudo-elements)
67
+ - 4.2 [Pseudo-Elements Over DOM Nodes](#42-pseudo-elements-over-dom-nodes)
68
+ - 4.3 [Position Relative Parent for Pseudo-Elements](#43-position-relative-parent-for-pseudo-elements)
69
+ - 4.4 [Z-Index Layering for Pseudo-Elements](#44-z-index-layering-for-pseudo-elements)
70
+ - 4.5 [Hit Target Expansion with Pseudo-Elements](#45-hit-target-expansion-with-pseudo-elements)
71
+ - 4.6 [View Transition Name Required](#46-view-transition-name-required)
72
+ - 4.7 [Unique View Transition Names](#47-unique-view-transition-names)
73
+ - 4.8 [Clean Up View Transition Names](#48-clean-up-view-transition-names)
74
+ - 4.9 [View Transitions Over JS Libraries](#49-view-transitions-over-js-libraries)
75
+ - 4.10 [Style View Transition Pseudo-Elements](#410-style-view-transition-pseudo-elements)
76
+ - 4.11 [Use ::backdrop for Dialog Backgrounds](#411-use-backdrop-for-dialog-backgrounds)
77
+ - 4.12 [Use ::placeholder for Input Styling](#412-use-placeholder-for-input-styling)
78
+ - 4.13 [Use ::selection for Text Styling](#413-use-selection-for-text-styling)
79
+ - 4.14 [Use ::marker for Custom List Bullets](#414-use-marker-for-custom-list-bullets)
80
+ - 4.15 [Use ::first-line for Typographic Treatments](#415-use-first-line-for-typographic-treatments)
81
+ 5. [Audio Feedback](#5-audio-feedback) — **MEDIUM**
82
+ - 5.1 [Visual Equivalent for Every Sound](#51-visual-equivalent-for-every-sound)
83
+ - 5.2 [Toggle Setting to Disable Sounds](#52-toggle-setting-to-disable-sounds)
84
+ - 5.3 [Respect prefers-reduced-motion for Sound](#53-respect-prefers-reduced-motion-for-sound)
85
+ - 5.4 [Independent Volume Control](#54-independent-volume-control)
86
+ - 5.5 [No Sound on High-Frequency Interactions](#55-no-sound-on-high-frequency-interactions)
87
+ - 5.6 [Sound for Confirmations](#56-sound-for-confirmations)
88
+ - 5.7 [Sound for Errors and Warnings](#57-sound-for-errors-and-warnings)
89
+ - 5.8 [No Decorative Sound](#58-no-decorative-sound)
90
+ - 5.9 [Informative Not Punishing Sound](#59-informative-not-punishing-sound)
91
+ - 5.10 [Preload Audio Files](#510-preload-audio-files)
92
+ - 5.11 [Subtle Default Volume](#511-subtle-default-volume)
93
+ - 5.12 [Reset currentTime Before Replay](#512-reset-currenttime-before-replay)
94
+ - 5.13 [Match Sound Weight to Action](#513-match-sound-weight-to-action)
95
+ - 5.14 [Sound Duration Matches Action Duration](#514-sound-duration-matches-action-duration)
96
+ 6. [Sound Synthesis](#6-sound-synthesis) — **MEDIUM**
97
+ - 6.1 [Reuse Single AudioContext](#61-reuse-single-audiocontext)
98
+ - 6.2 [Resume Suspended AudioContext](#62-resume-suspended-audiocontext)
99
+ - 6.3 [Clean Up Audio Nodes After Playback](#63-clean-up-audio-nodes-after-playback)
100
+ - 6.4 [Exponential Decay for Natural Sound](#64-exponential-decay-for-natural-sound)
101
+ - 6.5 [No Zero Target for Exponential Ramps](#65-no-zero-target-for-exponential-ramps)
102
+ - 6.6 [Set Initial Value Before Ramp](#66-set-initial-value-before-ramp)
103
+ - 6.7 [Noise for Percussive Sounds](#67-noise-for-percussive-sounds)
104
+ - 6.8 [Oscillators for Tonal Sounds](#68-oscillators-for-tonal-sounds)
105
+ - 6.9 [Bandpass Filter for Sound Character](#69-bandpass-filter-for-sound-character)
106
+ - 6.10 [Click Duration 5-15ms](#610-click-duration-5-15ms)
107
+ - 6.11 [Click Filter 3000-6000Hz](#611-click-filter-3000-6000hz)
108
+ - 6.12 [Gain Under 1.0](#612-gain-under-10)
109
+ - 6.13 [Filter Q Value 2-5](#613-filter-q-value-2-5)
110
+ 7. [Morphing Icons](#7-morphing-icons) — **LOW**
111
+ - 7.1 [Icons Must Use Exactly Three Lines](#71-icons-must-use-exactly-three-lines)
112
+ - 7.2 [Use Collapsed Constant for Unused Lines](#72-use-collapsed-constant-for-unused-lines)
113
+ - 7.3 [Consistent ViewBox Size](#73-consistent-viewbox-size)
114
+ - 7.4 [Shared Group for Rotational Variants](#74-shared-group-for-rotational-variants)
115
+ - 7.5 [Spring Physics for Rotation](#75-spring-physics-for-rotation)
116
+ - 7.6 [Reduced Motion Support for Icons](#76-reduced-motion-support-for-icons)
117
+ - 7.7 [Instant Jump for Non-Grouped Icons](#77-instant-jump-for-non-grouped-icons)
118
+ - 7.8 [Round Stroke Line Caps](#78-round-stroke-line-caps)
119
+ - 7.9 [Aria Hidden on Icon SVGs](#79-aria-hidden-on-icon-svgs)
120
+ 8. [Container Animation](#8-container-animation) — **MEDIUM**
121
+ - 8.1 [Two-Div Pattern for Animated Bounds](#81-two-div-pattern-for-animated-bounds)
122
+ - 8.2 [Guard Against Zero on Initial Render](#82-guard-against-zero-on-initial-render)
123
+ - 8.3 [Use ResizeObserver for Measurement](#83-use-resizeobserver-for-measurement)
124
+ - 8.4 [Overflow Hidden on Animated Container](#84-overflow-hidden-on-animated-container)
125
+ - 8.5 [Use Animated Bounds Sparingly](#85-use-animated-bounds-sparingly)
126
+ - 8.6 [Use Callback Ref for Measurement](#86-use-callback-ref-for-measurement)
127
+ - 8.7 [Add Delay for Natural Container Transitions](#87-add-delay-for-natural-container-transitions)
128
+ 9. [Laws of UX](#9-laws-of-ux) — **HIGH**
129
+ - 9.1 [Size Interactive Targets for Easy Clicking](#91-size-interactive-targets-for-easy-clicking)
130
+ - 9.2 [Expand Hit Areas with Invisible Padding](#92-expand-hit-areas-with-invisible-padding)
131
+ - 9.3 [Minimize Choices to Reduce Decision Time](#93-minimize-choices-to-reduce-decision-time)
132
+ - 9.4 [Chunk Data into Groups of 5-9](#94-chunk-data-into-groups-of-5-9)
133
+ - 9.5 [Respond Within 400ms](#95-respond-within-400ms)
134
+ - 9.6 [Fake Speed When Actual Speed Isn't Possible](#96-fake-speed-when-actual-speed-isnt-possible)
135
+ - 9.7 [Accept Messy Input, Output Clean Data](#97-accept-messy-input-output-clean-data)
136
+ - 9.8 [Show What Matters Now, Reveal Complexity Later](#98-show-what-matters-now-reveal-complexity-later)
137
+ - 9.9 [Use Familiar UI Patterns](#99-use-familiar-ui-patterns)
138
+ - 9.10 [Visual Polish Increases Perceived Usability](#910-visual-polish-increases-perceived-usability)
139
+ - 9.11 [Group Related Elements Spatially](#911-group-related-elements-spatially)
140
+ - 9.12 [Similar Elements Should Look Alike](#912-similar-elements-should-look-alike)
141
+ - 9.13 [Use Boundaries to Group Related Content](#913-use-boundaries-to-group-related-content)
142
+ - 9.14 [Make Important Elements Visually Distinct](#914-make-important-elements-visually-distinct)
143
+ - 9.15 [Place Key Items First or Last](#915-place-key-items-first-or-last)
144
+ - 9.16 [End Experiences with Clear Success States](#916-end-experiences-with-clear-success-states)
145
+ - 9.17 [Move Complexity to the System](#917-move-complexity-to-the-system)
146
+ - 9.18 [Show Progress Toward Completion](#918-show-progress-toward-completion)
147
+ - 9.19 [Show Incomplete State to Drive Completion](#919-show-incomplete-state-to-drive-completion)
148
+ - 9.20 [Simplify Complex Visuals into Clear Forms](#920-simplify-complex-visuals-into-clear-forms)
149
+ - 9.21 [Prioritize the Critical 20% of Features](#921-prioritize-the-critical-20-of-features)
150
+ - 9.22 [Minimize Extraneous Cognitive Load](#922-minimize-extraneous-cognitive-load)
151
+ - 9.23 [Visually Connect Related Elements](#923-visually-connect-related-elements)
152
+ 10. [Predictive Prefetching](#10-predictive-prefetching) — **MEDIUM**
153
+ - 10.1 [Trajectory Prediction Over Hover Prefetching](#101-trajectory-prediction-over-hover-prefetching)
154
+ - 10.2 [Prefetch by Intent, Not Viewport](#102-prefetch-by-intent-not-viewport)
155
+ - 10.3 [Use hitSlop to Trigger Predictions Earlier](#103-use-hitslop-to-trigger-predictions-earlier)
156
+ - 10.4 [Fall Back Gracefully on Touch Devices](#104-fall-back-gracefully-on-touch-devices)
157
+ - 10.5 [Prefetch on Keyboard Navigation](#105-prefetch-on-keyboard-navigation)
158
+ - 10.6 [Use Predictive Prefetching Selectively](#106-use-predictive-prefetching-selectively)
159
+ 11. [Typography](#11-typography) — **MEDIUM**
160
+ - 11.1 [Tabular Numbers for Data Display](#111-tabular-numbers-for-data-display)
161
+ - 11.2 [Oldstyle Numbers for Body Text](#112-oldstyle-numbers-for-body-text)
162
+ - 11.3 [Slashed Zero for Disambiguation](#113-slashed-zero-for-disambiguation)
163
+ - 11.4 [Enable Contextual Alternates](#114-enable-contextual-alternates)
164
+ - 11.5 [Use Disambiguation Stylistic Set for UI](#115-use-disambiguation-stylistic-set-for-ui)
165
+ - 11.6 [Keep Optical Sizing Auto](#116-keep-optical-sizing-auto)
166
+ - 11.7 [Use Antialiased Font Smoothing](#117-use-antialiased-font-smoothing)
167
+ - 11.8 [Balance Headings with text-wrap](#118-balance-headings-with-text-wrap)
168
+ - 11.9 [Offset Underlines from Descenders](#119-offset-underlines-from-descenders)
169
+ - 11.10 [Disable Font Synthesis for Missing Styles](#1110-disable-font-synthesis-for-missing-styles)
170
+ - 11.11 [Use font-display swap](#1111-use-font-display-swap)
171
+ - 11.12 [Continuous Weight Values with Variable Fonts](#1112-continuous-weight-values-with-variable-fonts)
172
+ - 11.13 [text-wrap pretty for Body Text](#1113-text-wrap-pretty-for-body-text)
173
+ - 11.14 [Pair Justified Text with Hyphens](#1114-pair-justified-text-with-hyphens)
174
+ - 11.15 [Add Letter Spacing to Uppercase Text](#1115-add-letter-spacing-to-uppercase-text)
175
+ - 11.16 [Use Typographic Fractions](#1116-use-typographic-fractions)
176
+ 12. [Visual Design](#12-visual-design) — **HIGH**
177
+ - 12.1 [Concentric Border Radius for Nested Elements](#121-concentric-border-radius-for-nested-elements)
178
+ - 12.2 [Layer Multiple Shadows for Realistic Depth](#122-layer-multiple-shadows-for-realistic-depth)
179
+ - 12.3 [Consistent Shadow Direction Across UI](#123-consistent-shadow-direction-across-ui)
180
+ - 12.4 [Use Neutral Colors for Shadows](#124-use-neutral-colors-for-shadows)
181
+ - 12.5 [Shadow Size Indicates Elevation](#125-shadow-size-indicates-elevation)
182
+ - 12.6 [Animate Shadows via Pseudo-Element Opacity](#126-animate-shadows-via-pseudo-element-opacity)
183
+ - 12.7 [Use a Consistent Spacing Scale](#127-use-a-consistent-spacing-scale)
184
+ - 12.8 [Use Semi-Transparent Borders](#128-use-semi-transparent-borders)
185
+ - 12.9 [Full Shadow Anatomy on Buttons](#129-full-shadow-anatomy-on-buttons)
186
+
187
+ ---
188
+
189
+ ## 1. Animation Principles
190
+
191
+ **Impact:** CRITICAL — Disney's 12 principles adapted for web. Violations here produce the most noticeable quality issues.
192
+
193
+ ### 1.1 User Animations Under 300ms
194
+
195
+ User-initiated animations must complete within 300ms.
196
+
197
+ **Incorrect (exceeds 300ms limit):**
198
+
199
+ ```css
200
+ .button { transition: transform 400ms; }
201
+ ```
202
+
203
+ **Correct (within 300ms):**
204
+
205
+ ```css
206
+ .button { transition: transform 200ms; }
207
+ ```
208
+
209
+ ### 1.2 Consistent Timing for Similar Elements
210
+
211
+ Similar elements must use identical timing values.
212
+
213
+ **Incorrect (inconsistent timing):**
214
+
215
+ ```css
216
+ .button-primary { transition: 200ms; }
217
+ .button-secondary { transition: 150ms; }
218
+ ```
219
+
220
+ **Correct (consistent timing):**
221
+
222
+ ```css
223
+ .button-primary { transition: 200ms; }
224
+ .button-secondary { transition: 200ms; }
225
+ ```
226
+
227
+ ### 1.3 No Entrance Animation on Context Menus
228
+
229
+ Context menus should not animate on entrance (exit only).
230
+
231
+ **Incorrect (animates entrance):**
232
+
233
+ ```tsx
234
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
235
+ ```
236
+
237
+ **Correct (exit only):**
238
+
239
+ ```tsx
240
+ <motion.div exit={{ opacity: 0 }} />
241
+ ```
242
+
243
+ ### 1.4 Exponential Ramps for Natural Decay
244
+
245
+ Use exponential ramps, not linear, for natural decay.
246
+
247
+ **Incorrect (linear ramp):**
248
+
249
+ ```ts
250
+ gain.gain.linearRampToValueAtTime(0, t + 0.05);
251
+ ```
252
+
253
+ **Correct (exponential ramp):**
254
+
255
+ ```ts
256
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
257
+ ```
258
+
259
+ ### 1.5 No Linear Easing for Motion
260
+
261
+ Linear easing should only be used for progress indicators, not motion.
262
+
263
+ **Incorrect (linear for motion):**
264
+
265
+ ```css
266
+ .card { transition: transform 200ms linear; }
267
+ ```
268
+
269
+ **Correct (linear for progress only):**
270
+
271
+ ```css
272
+ .progress-bar { transition: width 100ms linear; }
273
+ ```
274
+
275
+ ### 1.6 Active State Scale Transform
276
+
277
+ Interactive elements must have active/pressed state with scale transform.
278
+
279
+ **Incorrect (no active state):**
280
+
281
+ ```css
282
+ .button:hover { background: var(--gray-3); }
283
+ /* Missing :active state */
284
+ ```
285
+
286
+ **Correct (active state present):**
287
+
288
+ ```css
289
+ .button:active { transform: scale(0.98); }
290
+ ```
291
+
292
+ ### 1.7 Subtle Squash and Stretch
293
+
294
+ Squash/stretch deformation must be subtle (0.95-1.05 range).
295
+
296
+ **Incorrect (excessive deformation):**
297
+
298
+ ```tsx
299
+ <motion.div whileTap={{ scale: 0.8 }} />
300
+ ```
301
+
302
+ **Correct (subtle deformation):**
303
+
304
+ ```tsx
305
+ <motion.div whileTap={{ scale: 0.98 }} />
306
+ ```
307
+
308
+ ### 1.8 Springs for Overshoot and Settle
309
+
310
+ Use springs (not easing) when overshoot-and-settle is needed.
311
+
312
+ **Incorrect (easing for bounce):**
313
+
314
+ ```tsx
315
+ <motion.div transition={{ duration: 0.3, ease: "easeOut" }} />
316
+ ```
317
+
318
+ **Correct (spring physics):**
319
+
320
+ ```tsx
321
+ <motion.div transition={{ type: "spring", stiffness: 500, damping: 30 }} />
322
+ ```
323
+
324
+ ### 1.9 Stagger Under 50ms Per Item
325
+
326
+ Stagger delays must not exceed 50ms per item.
327
+
328
+ **Incorrect (excessive stagger):**
329
+
330
+ ```tsx
331
+ transition={{ staggerChildren: 0.15 }}
332
+ ```
333
+
334
+ **Correct (reasonable stagger):**
335
+
336
+ ```tsx
337
+ transition={{ staggerChildren: 0.03 }}
338
+ ```
339
+
340
+ ### 1.10 Single Focal Point
341
+
342
+ Only one element should animate prominently at a time.
343
+
344
+ **Incorrect (competing animations):**
345
+
346
+ ```tsx
347
+ <motion.div animate={{ scale: 1.1 }} />
348
+ <motion.div animate={{ scale: 1.1 }} />
349
+ ```
350
+
351
+ ### 1.11 Dim Background for Focus
352
+
353
+ Modal/dialog backgrounds should dim to direct focus.
354
+
355
+ **Incorrect (transparent overlay):**
356
+
357
+ ```css
358
+ .overlay { background: transparent; }
359
+ ```
360
+
361
+ **Correct (dimmed overlay):**
362
+
363
+ ```css
364
+ .overlay { background: var(--black-a6); }
365
+ ```
366
+
367
+ ### 1.12 Z-Index Layering for Animated Elements
368
+
369
+ Animated elements must respect z-index layering.
370
+
371
+ **Incorrect (no z-index):**
372
+
373
+ ```css
374
+ .tooltip { /* No z-index, may render behind other elements */ }
375
+ ```
376
+
377
+ **Correct (explicit z-index):**
378
+
379
+ ```css
380
+ .tooltip { z-index: 50; }
381
+ ```
382
+
383
+ ---
384
+
385
+ ## 2. Timing Functions
386
+
387
+ **Impact:** HIGH — Choosing the right timing function based on whether motion is user-driven, system-driven, or high-frequency.
388
+
389
+ **Decision framework:** Is this motion reacting to the user, or is the system speaking?
390
+
391
+ | Motion Type | Best Choice | Why |
392
+ |-------------|-------------|-----|
393
+ | User-driven (drag, flick, gesture) | Spring | Survives interruption, preserves velocity |
394
+ | System-driven (state change, feedback) | Easing | Clear start/end, predictable timing |
395
+ | Time representation (progress, loading) | Linear | 1:1 relationship between time and progress |
396
+ | High-frequency (typing, fast toggles) | None | Animation adds noise, feels slower |
397
+
398
+ ### 2.1 Springs for Gesture-Driven Motion
399
+
400
+ Gesture-driven motion (drag, flick, swipe) must use springs.
401
+
402
+ **Incorrect (easing for drag):**
403
+
404
+ ```tsx
405
+ <motion.div
406
+ drag="x"
407
+ transition={{ duration: 0.3, ease: "easeOut" }}
408
+ />
409
+ ```
410
+
411
+ **Correct (spring for drag):**
412
+
413
+ ```tsx
414
+ <motion.div
415
+ drag="x"
416
+ transition={{ type: "spring", stiffness: 500, damping: 30 }}
417
+ />
418
+ ```
419
+
420
+ ### 2.2 Springs for Interruptible Motion
421
+
422
+ Motion that can be interrupted must use springs.
423
+
424
+ **Incorrect (easing for interruptible):**
425
+
426
+ ```tsx
427
+ <motion.div
428
+ animate={{ x: isOpen ? 200 : 0 }}
429
+ transition={{ duration: 0.3 }}
430
+ />
431
+ ```
432
+
433
+ **Correct (spring for interruptible):**
434
+
435
+ ```tsx
436
+ <motion.div
437
+ animate={{ x: isOpen ? 200 : 0 }}
438
+ transition={{ type: "spring", stiffness: 400, damping: 25 }}
439
+ />
440
+ ```
441
+
442
+ ### 2.3 Springs Preserve Input Velocity
443
+
444
+ When velocity matters, use springs to preserve input energy.
445
+
446
+ **Incorrect (velocity ignored):**
447
+
448
+ ```tsx
449
+ onDragEnd={(e, info) => {
450
+ animate(target, { x: 0 }, { duration: 0.3 });
451
+ }}
452
+ ```
453
+
454
+ **Correct (velocity preserved):**
455
+
456
+ ```tsx
457
+ onDragEnd={(e, info) => {
458
+ animate(target, { x: 0 }, {
459
+ type: "spring",
460
+ velocity: info.velocity.x,
461
+ });
462
+ }}
463
+ ```
464
+
465
+ ### 2.4 Balanced Spring Parameters
466
+
467
+ Spring parameters must be balanced; avoid excessive oscillation.
468
+
469
+ **Incorrect (too bouncy):**
470
+
471
+ ```tsx
472
+ transition={{
473
+ type: "spring",
474
+ stiffness: 1000,
475
+ damping: 5,
476
+ }}
477
+ ```
478
+
479
+ **Correct (balanced):**
480
+
481
+ ```tsx
482
+ transition={{
483
+ type: "spring",
484
+ stiffness: 500,
485
+ damping: 30,
486
+ }}
487
+ ```
488
+
489
+ ### 2.5 Easing for System State Changes
490
+
491
+ System-initiated state changes should use easing curves.
492
+
493
+ **Incorrect (spring for announcement):**
494
+
495
+ ```tsx
496
+ <motion.div
497
+ animate={{ y: 0 }}
498
+ transition={{ type: "spring" }}
499
+ />
500
+ ```
501
+
502
+ **Correct (easing for announcement):**
503
+
504
+ ```tsx
505
+ <motion.div
506
+ animate={{ y: 0 }}
507
+ transition={{ duration: 0.2, ease: "easeOut" }}
508
+ />
509
+ ```
510
+
511
+ ### 2.6 Ease-Out for Entrances
512
+
513
+ Entrances must use ease-out (arrive fast, settle gently).
514
+
515
+ **Incorrect (ease-in for entrance):**
516
+
517
+ ```css
518
+ .modal-enter { animation-timing-function: ease-in; }
519
+ ```
520
+
521
+ **Correct (ease-out for entrance):**
522
+
523
+ ```css
524
+ .modal-enter { animation-timing-function: ease-out; }
525
+ ```
526
+
527
+ ### 2.7 Ease-In for Exits
528
+
529
+ Exits must use ease-in (build momentum before departure).
530
+
531
+ **Incorrect (ease-out for exit):**
532
+
533
+ ```css
534
+ .modal-exit { animation-timing-function: ease-out; }
535
+ ```
536
+
537
+ **Correct (ease-in for exit):**
538
+
539
+ ```css
540
+ .modal-exit { animation-timing-function: ease-in; }
541
+ ```
542
+
543
+ ### 2.8 Ease-In-Out for View Transitions
544
+
545
+ View/mode transitions use ease-in-out for neutral attention.
546
+
547
+ **Correct:**
548
+
549
+ ```css
550
+ .page-transition { animation-timing-function: ease-in-out; }
551
+ ```
552
+
553
+ ### 2.9 Linear Easing Only for Progress
554
+
555
+ Linear easing only for progress bars and time representation.
556
+
557
+ **Incorrect (linear for motion):**
558
+
559
+ ```css
560
+ .card-slide { transition: transform 200ms linear; }
561
+ ```
562
+
563
+ **Correct (linear for progress):**
564
+
565
+ ```css
566
+ .progress-bar { transition: width 100ms linear; }
567
+ ```
568
+
569
+ ### 2.10 Press and Hover 120-180ms
570
+
571
+ Press and hover interactions should use 120-180ms duration.
572
+
573
+ **Incorrect (too slow):**
574
+
575
+ ```css
576
+ .button:hover { transition: background-color 400ms; }
577
+ ```
578
+
579
+ **Correct (appropriate duration):**
580
+
581
+ ```css
582
+ .button:hover { transition: background-color 150ms; }
583
+ ```
584
+
585
+ ### 2.11 Small State Changes 180-260ms
586
+
587
+ Small state changes should use 180-260ms duration.
588
+
589
+ **Correct:**
590
+
591
+ ```css
592
+ .toggle { transition: transform 200ms ease; }
593
+ ```
594
+
595
+ ### 2.12 Max 300ms for User Actions
596
+
597
+ User-initiated animations must not exceed 300ms.
598
+
599
+ **Incorrect (exceeds limit):**
600
+
601
+ ```tsx
602
+ <motion.div transition={{ duration: 0.5 }} />
603
+ ```
604
+
605
+ **Correct (within limit):**
606
+
607
+ ```tsx
608
+ <motion.div transition={{ duration: 0.25 }} />
609
+ ```
610
+
611
+ ### 2.13 Shorten Duration Before Adjusting Curve
612
+
613
+ If animation feels slow, shorten duration before adjusting curve.
614
+
615
+ **Incorrect (adjusting curve instead):**
616
+
617
+ ```css
618
+ .element { transition: 400ms cubic-bezier(0, 0.9, 0.1, 1); }
619
+ ```
620
+
621
+ **Correct (shorter duration):**
622
+
623
+ ```css
624
+ .element { transition: 200ms ease-out; }
625
+ ```
626
+
627
+ ### 2.14 No Animation for High-Frequency Interactions
628
+
629
+ High-frequency interactions should have no animation.
630
+
631
+ **Incorrect (animated on every keystroke):**
632
+
633
+ ```tsx
634
+ function SearchInput() {
635
+ return (
636
+ <motion.div animate={{ scale: [1, 1.02, 1] }}>
637
+ <input onChange={handleSearch} />
638
+ </motion.div>
639
+ );
640
+ }
641
+ ```
642
+
643
+ **Correct (no animation):**
644
+
645
+ ```tsx
646
+ function SearchInput() {
647
+ return <input onChange={handleSearch} />;
648
+ }
649
+ ```
650
+
651
+ ### 2.15 No Animation for Keyboard Navigation
652
+
653
+ Keyboard navigation should be instant, no animation.
654
+
655
+ **Incorrect (animated focus):**
656
+
657
+ ```tsx
658
+ function Menu() {
659
+ return items.map(item => (
660
+ <motion.li
661
+ whileFocus={{ scale: 1.05 }}
662
+ transition={{ duration: 0.2 }}
663
+ />
664
+ ));
665
+ }
666
+ ```
667
+
668
+ **Correct (CSS focus-visible only):**
669
+
670
+ ```tsx
671
+ function Menu() {
672
+ return items.map(item => (
673
+ <li className={styles.menuItem} />
674
+ ));
675
+ }
676
+ ```
677
+
678
+ ### 2.16 No Entrance Animation for Context Menus
679
+
680
+ Context menus should not animate on entrance (exit only).
681
+
682
+ **Incorrect (entrance animation):**
683
+
684
+ ```tsx
685
+ <motion.div
686
+ initial={{ opacity: 0, scale: 0.95 }}
687
+ animate={{ opacity: 1, scale: 1 }}
688
+ exit={{ opacity: 0 }}
689
+ />
690
+ ```
691
+
692
+ **Correct (exit only):**
693
+
694
+ ```tsx
695
+ <motion.div exit={{ opacity: 0, scale: 0.95 }} />
696
+ ```
697
+
698
+ **Quick reference:**
699
+
700
+ | Interaction | Timing | Type |
701
+ |-------------|--------|------|
702
+ | Drag release | Spring | `stiffness: 500, damping: 30` |
703
+ | Button press | 150ms | `ease` |
704
+ | Modal enter | 200ms | `ease-out` |
705
+ | Modal exit | 150ms | `ease-in` |
706
+ | Page transition | 250ms | `ease-in-out` |
707
+ | Progress bar | varies | `linear` |
708
+ | Typing feedback | 0ms | none |
709
+
710
+ ---
711
+
712
+ ## 3. Exit Animations
713
+
714
+ **Impact:** HIGH — Correct AnimatePresence usage prevents layout shifts, stale interactions, and orphaned elements.
715
+
716
+ ### 3.1 AnimatePresence Wrapper Required
717
+
718
+ Conditional motion elements must be wrapped in AnimatePresence.
719
+
720
+ **Incorrect (no wrapper):**
721
+
722
+ ```tsx
723
+ {isVisible && (
724
+ <motion.div exit={{ opacity: 0 }} />
725
+ )}
726
+ ```
727
+
728
+ **Correct (wrapped):**
729
+
730
+ ```tsx
731
+ <AnimatePresence>
732
+ {isVisible && (
733
+ <motion.div exit={{ opacity: 0 }} />
734
+ )}
735
+ </AnimatePresence>
736
+ ```
737
+
738
+ ### 3.2 Exit Prop Required Inside AnimatePresence
739
+
740
+ Elements inside AnimatePresence should have exit prop defined.
741
+
742
+ **Incorrect (missing exit):**
743
+
744
+ ```tsx
745
+ <AnimatePresence>
746
+ {isOpen && (
747
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
748
+ )}
749
+ </AnimatePresence>
750
+ ```
751
+
752
+ **Correct (exit defined):**
753
+
754
+ ```tsx
755
+ <AnimatePresence>
756
+ {isOpen && (
757
+ <motion.div
758
+ initial={{ opacity: 0 }}
759
+ animate={{ opacity: 1 }}
760
+ exit={{ opacity: 0 }}
761
+ />
762
+ )}
763
+ </AnimatePresence>
764
+ ```
765
+
766
+ ### 3.3 Unique Keys in AnimatePresence Lists
767
+
768
+ Dynamic lists inside AnimatePresence must have unique keys.
769
+
770
+ **Incorrect (index as key):**
771
+
772
+ ```tsx
773
+ <AnimatePresence>
774
+ {items.map((item, index) => (
775
+ <motion.div key={index} exit={{ opacity: 0 }} />
776
+ ))}
777
+ </AnimatePresence>
778
+ ```
779
+
780
+ **Correct (stable unique key):**
781
+
782
+ ```tsx
783
+ <AnimatePresence>
784
+ {items.map((item) => (
785
+ <motion.div key={item.id} exit={{ opacity: 0 }} />
786
+ ))}
787
+ </AnimatePresence>
788
+ ```
789
+
790
+ ### 3.4 Exit Mirrors Initial for Symmetry
791
+
792
+ Exit animation should mirror initial for symmetry.
793
+
794
+ **Incorrect (asymmetric exit):**
795
+
796
+ ```tsx
797
+ <motion.div
798
+ initial={{ opacity: 0, y: 20 }}
799
+ animate={{ opacity: 1, y: 0 }}
800
+ exit={{ scale: 0 }}
801
+ />
802
+ ```
803
+
804
+ **Correct (symmetric exit):**
805
+
806
+ ```tsx
807
+ <motion.div
808
+ initial={{ opacity: 0, y: 20 }}
809
+ animate={{ opacity: 1, y: 0 }}
810
+ exit={{ opacity: 0, y: 20 }}
811
+ />
812
+ ```
813
+
814
+ ### 3.5 useIsPresent in Child Component
815
+
816
+ useIsPresent must be called from child of AnimatePresence, not parent.
817
+
818
+ **Incorrect (hook in parent):**
819
+
820
+ ```tsx
821
+ function Parent() {
822
+ const isPresent = useIsPresent();
823
+ return (
824
+ <AnimatePresence>
825
+ {show && <Child />}
826
+ </AnimatePresence>
827
+ );
828
+ }
829
+ ```
830
+
831
+ **Correct (hook in child):**
832
+
833
+ ```tsx
834
+ function Child() {
835
+ const isPresent = useIsPresent();
836
+ return <motion.div data-exiting={!isPresent} />;
837
+ }
838
+ ```
839
+
840
+ ### 3.6 Call safeToRemove After Async Work
841
+
842
+ When using usePresence, always call safeToRemove after async work.
843
+
844
+ **Incorrect (missing safeToRemove):**
845
+
846
+ ```tsx
847
+ function AsyncComponent() {
848
+ const [isPresent, safeToRemove] = usePresence();
849
+
850
+ useEffect(() => {
851
+ if (!isPresent) {
852
+ cleanup();
853
+ }
854
+ }, [isPresent]);
855
+ }
856
+ ```
857
+
858
+ **Correct (safeToRemove called):**
859
+
860
+ ```tsx
861
+ function AsyncComponent() {
862
+ const [isPresent, safeToRemove] = usePresence();
863
+
864
+ useEffect(() => {
865
+ if (!isPresent) {
866
+ cleanup().then(safeToRemove);
867
+ }
868
+ }, [isPresent, safeToRemove]);
869
+ }
870
+ ```
871
+
872
+ ### 3.7 Disable Interactions on Exiting Elements
873
+
874
+ Disable interactions on exiting elements using isPresent.
875
+
876
+ **Incorrect (clickable during exit):**
877
+
878
+ ```tsx
879
+ function Card() {
880
+ const isPresent = useIsPresent();
881
+ return <button onClick={handleClick}>Click</button>;
882
+ }
883
+ ```
884
+
885
+ **Correct (disabled during exit):**
886
+
887
+ ```tsx
888
+ function Card() {
889
+ const isPresent = useIsPresent();
890
+ return (
891
+ <button onClick={handleClick} disabled={!isPresent}>
892
+ Click
893
+ </button>
894
+ );
895
+ }
896
+ ```
897
+
898
+ ### 3.8 Mode "wait" Doubles Duration
899
+
900
+ Mode "wait" nearly doubles animation duration; adjust timing accordingly.
901
+
902
+ **Incorrect (too slow with wait):**
903
+
904
+ ```tsx
905
+ <AnimatePresence mode="wait">
906
+ <motion.div transition={{ duration: 0.3 }} />
907
+ </AnimatePresence>
908
+ ```
909
+
910
+ **Correct (halved timing):**
911
+
912
+ ```tsx
913
+ <AnimatePresence mode="wait">
914
+ <motion.div transition={{ duration: 0.15 }} />
915
+ </AnimatePresence>
916
+ ```
917
+
918
+ ### 3.9 Mode "sync" Causes Layout Conflicts
919
+
920
+ Mode "sync" causes layout conflicts; position exiting elements absolutely.
921
+
922
+ **Incorrect (sync with layout competition):**
923
+
924
+ ```tsx
925
+ <AnimatePresence mode="sync">
926
+ {items.map(item => (
927
+ <motion.div exit={{ opacity: 0 }}>{item}</motion.div>
928
+ ))}
929
+ </AnimatePresence>
930
+ ```
931
+
932
+ **Correct (popLayout instead):**
933
+
934
+ ```tsx
935
+ <AnimatePresence mode="popLayout">
936
+ {items.map(item => (
937
+ <motion.div exit={{ opacity: 0 }}>{item}</motion.div>
938
+ ))}
939
+ </AnimatePresence>
940
+ ```
941
+
942
+ ### 3.10 popLayout for List Reordering
943
+
944
+ Use popLayout mode for list reordering animations.
945
+
946
+ **Incorrect (default mode causes shifts):**
947
+
948
+ ```tsx
949
+ <AnimatePresence>
950
+ {items.map(item => <ListItem key={item.id} />)}
951
+ </AnimatePresence>
952
+ ```
953
+
954
+ **Correct (popLayout prevents shifts):**
955
+
956
+ ```tsx
957
+ <AnimatePresence mode="popLayout">
958
+ {items.map(item => <ListItem key={item.id} />)}
959
+ </AnimatePresence>
960
+ ```
961
+
962
+ ### 3.11 Propagate Prop for Nested AnimatePresence
963
+
964
+ Nested AnimatePresence must use propagate prop for coordinated exits.
965
+
966
+ **Incorrect (children vanish instantly):**
967
+
968
+ ```tsx
969
+ <AnimatePresence>
970
+ {isOpen && (
971
+ <motion.div exit={{ opacity: 0 }}>
972
+ <AnimatePresence>
973
+ {items.map(item => (
974
+ <motion.div key={item.id} exit={{ scale: 0 }} />
975
+ ))}
976
+ </AnimatePresence>
977
+ </motion.div>
978
+ )}
979
+ </AnimatePresence>
980
+ ```
981
+
982
+ **Correct (propagate on both):**
983
+
984
+ ```tsx
985
+ <AnimatePresence propagate>
986
+ {isOpen && (
987
+ <motion.div exit={{ opacity: 0 }}>
988
+ <AnimatePresence propagate>
989
+ {items.map(item => (
990
+ <motion.div key={item.id} exit={{ scale: 0 }} />
991
+ ))}
992
+ </AnimatePresence>
993
+ </motion.div>
994
+ )}
995
+ </AnimatePresence>
996
+ ```
997
+
998
+ ### 3.12 Coordinated Parent-Child Exit Timing
999
+
1000
+ Parent and child exit durations should be coordinated.
1001
+
1002
+ **Incorrect (parent too fast):**
1003
+
1004
+ ```tsx
1005
+ <motion.div exit={{ opacity: 0 }} transition={{ duration: 0.1 }}>
1006
+ <motion.div exit={{ scale: 0 }} transition={{ duration: 0.5 }} />
1007
+ </motion.div>
1008
+ ```
1009
+
1010
+ **Correct (coordinated timing):**
1011
+
1012
+ ```tsx
1013
+ <motion.div exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
1014
+ <motion.div exit={{ scale: 0 }} transition={{ duration: 0.15 }} />
1015
+ </motion.div>
1016
+ ```
1017
+
1018
+ Reference: [Motion AnimatePresence Documentation](https://motion.dev/docs/react-animate-presence)
1019
+
1020
+ ---
1021
+
1022
+ ## 4. CSS Pseudo Elements
1023
+
1024
+ **Impact:** MEDIUM — Leveraging pseudo-elements and View Transitions to reduce DOM nodes and improve transitions.
1025
+
1026
+ ### 4.1 Content Property Required for Pseudo-Elements
1027
+
1028
+ ::before and ::after require content property to render.
1029
+
1030
+ **Incorrect (missing content):**
1031
+
1032
+ ```css
1033
+ .button::before {
1034
+ position: absolute;
1035
+ background: var(--gray-3);
1036
+ }
1037
+ ```
1038
+
1039
+ **Correct (content set):**
1040
+
1041
+ ```css
1042
+ .button::before {
1043
+ content: "";
1044
+ position: absolute;
1045
+ background: var(--gray-3);
1046
+ }
1047
+ ```
1048
+
1049
+ ### 4.2 Pseudo-Elements Over DOM Nodes
1050
+
1051
+ Use pseudo-elements for decorative content instead of extra DOM nodes.
1052
+
1053
+ **Incorrect (extra DOM node):**
1054
+
1055
+ ```tsx
1056
+ <button className={styles.button}>
1057
+ <span className={styles.background} />
1058
+ Click me
1059
+ </button>
1060
+ ```
1061
+
1062
+ **Correct (pseudo-element):**
1063
+
1064
+ ```tsx
1065
+ <button className={styles.button}>
1066
+ Click me
1067
+ </button>
1068
+ ```
1069
+
1070
+ ```css
1071
+ .button::before {
1072
+ content: "";
1073
+ /* decorative background */
1074
+ }
1075
+ ```
1076
+
1077
+ ### 4.3 Position Relative Parent for Pseudo-Elements
1078
+
1079
+ Parent must have position: relative for absolute pseudo-elements.
1080
+
1081
+ **Incorrect (no position on parent):**
1082
+
1083
+ ```css
1084
+ .button::before {
1085
+ content: "";
1086
+ position: absolute;
1087
+ inset: 0;
1088
+ }
1089
+ /* .button has no position */
1090
+ ```
1091
+
1092
+ **Correct (parent positioned):**
1093
+
1094
+ ```css
1095
+ .button {
1096
+ position: relative;
1097
+ }
1098
+
1099
+ .button::before {
1100
+ content: "";
1101
+ position: absolute;
1102
+ inset: 0;
1103
+ }
1104
+ ```
1105
+
1106
+ ### 4.4 Z-Index Layering for Pseudo-Elements
1107
+
1108
+ Pseudo-elements need z-index to layer correctly with content.
1109
+
1110
+ **Incorrect (covers button text):**
1111
+
1112
+ ```css
1113
+ .button::before {
1114
+ content: "";
1115
+ position: absolute;
1116
+ inset: 0;
1117
+ background: var(--gray-3);
1118
+ }
1119
+ ```
1120
+
1121
+ **Correct (layered behind):**
1122
+
1123
+ ```css
1124
+ .button {
1125
+ position: relative;
1126
+ z-index: 1;
1127
+ }
1128
+
1129
+ .button::before {
1130
+ content: "";
1131
+ position: absolute;
1132
+ inset: 0;
1133
+ background: var(--gray-3);
1134
+ z-index: -1;
1135
+ }
1136
+ ```
1137
+
1138
+ ### 4.5 Hit Target Expansion with Pseudo-Elements
1139
+
1140
+ Use negative inset values to expand hit targets without extra markup.
1141
+
1142
+ **Incorrect (wrapper for hit target):**
1143
+
1144
+ ```tsx
1145
+ <div className={styles.wrapper}>
1146
+ <a className={styles.link}>Link</a>
1147
+ </div>
1148
+ ```
1149
+
1150
+ **Correct (pseudo-element expansion):**
1151
+
1152
+ ```css
1153
+ .link {
1154
+ position: relative;
1155
+ }
1156
+
1157
+ .link::before {
1158
+ content: "";
1159
+ position: absolute;
1160
+ inset: -8px -12px;
1161
+ }
1162
+ ```
1163
+
1164
+ ### 4.6 View Transition Name Required
1165
+
1166
+ Elements participating in view transitions need view-transition-name.
1167
+
1168
+ **Incorrect (no transition name):**
1169
+
1170
+ ```ts
1171
+ document.startViewTransition(() => {
1172
+ targetImg.src = newSrc;
1173
+ });
1174
+ ```
1175
+
1176
+ **Correct (transition name assigned):**
1177
+
1178
+ ```ts
1179
+ sourceImg.style.viewTransitionName = "card";
1180
+ document.startViewTransition(() => {
1181
+ sourceImg.style.viewTransitionName = "";
1182
+ targetImg.style.viewTransitionName = "card";
1183
+ });
1184
+ ```
1185
+
1186
+ ### 4.7 Unique View Transition Names
1187
+
1188
+ Each view-transition-name must be unique on the page during transition.
1189
+
1190
+ **Incorrect (duplicate names):**
1191
+
1192
+ ```css
1193
+ .card {
1194
+ view-transition-name: card;
1195
+ }
1196
+ /* Multiple cards with same name */
1197
+ ```
1198
+
1199
+ **Correct (unique per element):**
1200
+
1201
+ ```ts
1202
+ element.style.viewTransitionName = `card-${id}`;
1203
+ ```
1204
+
1205
+ ### 4.8 Clean Up View Transition Names
1206
+
1207
+ Remove view-transition-name after transition completes.
1208
+
1209
+ **Incorrect (stale name):**
1210
+
1211
+ ```ts
1212
+ sourceImg.style.viewTransitionName = "card";
1213
+ document.startViewTransition(() => {
1214
+ targetImg.style.viewTransitionName = "card";
1215
+ });
1216
+ ```
1217
+
1218
+ **Correct (name cleaned up):**
1219
+
1220
+ ```ts
1221
+ sourceImg.style.viewTransitionName = "card";
1222
+ document.startViewTransition(() => {
1223
+ sourceImg.style.viewTransitionName = "";
1224
+ targetImg.style.viewTransitionName = "card";
1225
+ });
1226
+ ```
1227
+
1228
+ ### 4.9 View Transitions Over JS Libraries
1229
+
1230
+ Prefer View Transitions API over JavaScript animation libraries for page transitions.
1231
+
1232
+ **Incorrect (JS-based transition):**
1233
+
1234
+ ```tsx
1235
+ import { motion } from "motion/react";
1236
+
1237
+ function ImageLightbox() {
1238
+ return (
1239
+ <motion.img layoutId="hero" />
1240
+ );
1241
+ }
1242
+ ```
1243
+
1244
+ **Correct (native View Transition):**
1245
+
1246
+ ```ts
1247
+ function openLightbox(img: HTMLImageElement) {
1248
+ img.style.viewTransitionName = "hero";
1249
+ document.startViewTransition(() => {
1250
+ // Native browser transition
1251
+ });
1252
+ }
1253
+ ```
1254
+
1255
+ ### 4.10 Style View Transition Pseudo-Elements
1256
+
1257
+ Style view transition pseudo-elements for custom animations.
1258
+
1259
+ **Incorrect (default crossfade only):**
1260
+
1261
+ ```ts
1262
+ document.startViewTransition(() => { /* ... */ });
1263
+ ```
1264
+
1265
+ **Correct (custom animation):**
1266
+
1267
+ ```css
1268
+ ::view-transition-group(card) {
1269
+ animation-duration: 300ms;
1270
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
1271
+ }
1272
+ ```
1273
+
1274
+ ### 4.11 Use ::backdrop for Dialog Backgrounds
1275
+
1276
+ Use ::backdrop pseudo-element for dialog/popover backgrounds.
1277
+
1278
+ **Incorrect (extra overlay node):**
1279
+
1280
+ ```tsx
1281
+ <>
1282
+ <div className={styles.overlay} onClick={close} />
1283
+ <dialog className={styles.dialog}>{children}</dialog>
1284
+ </>
1285
+ ```
1286
+
1287
+ **Correct (native ::backdrop):**
1288
+
1289
+ ```css
1290
+ dialog::backdrop {
1291
+ background: var(--black-a6);
1292
+ backdrop-filter: blur(4px);
1293
+ }
1294
+ ```
1295
+
1296
+ ### 4.12 Use ::placeholder for Input Styling
1297
+
1298
+ Use ::placeholder for input placeholder styling, not wrapper elements.
1299
+
1300
+ **Incorrect (custom placeholder node):**
1301
+
1302
+ ```tsx
1303
+ <div className={styles.inputWrapper}>
1304
+ {!value && <span className={styles.placeholder}>Enter text...</span>}
1305
+ <input value={value} />
1306
+ </div>
1307
+ ```
1308
+
1309
+ **Correct (native ::placeholder):**
1310
+
1311
+ ```css
1312
+ input::placeholder {
1313
+ color: var(--gray-9);
1314
+ opacity: 1;
1315
+ }
1316
+ ```
1317
+
1318
+ ### 4.13 Use ::selection for Text Styling
1319
+
1320
+ Use ::selection for text selection styling.
1321
+
1322
+ **Correct:**
1323
+
1324
+ ```css
1325
+ ::selection {
1326
+ background: var(--blue-a5);
1327
+ color: var(--gray-12);
1328
+ }
1329
+ ```
1330
+
1331
+ ### 4.14 Use ::marker for Custom List Bullets
1332
+
1333
+ Use ::marker to style list bullets without extra elements or background-image hacks.
1334
+
1335
+ **Incorrect (background image hack):**
1336
+
1337
+ ```css
1338
+ li {
1339
+ list-style: none;
1340
+ background: url("bullet.svg") no-repeat 0 4px;
1341
+ padding-left: 20px;
1342
+ }
1343
+ ```
1344
+
1345
+ **Correct (native ::marker):**
1346
+
1347
+ ```css
1348
+ li::marker {
1349
+ color: var(--gray-8);
1350
+ font-size: 0.8em;
1351
+ }
1352
+ ```
1353
+
1354
+ ### 4.15 Use ::first-line for Typographic Treatments
1355
+
1356
+ Use ::first-line for drop-cap-adjacent styling without JavaScript or hardcoded spans.
1357
+
1358
+ **Incorrect (manual span):**
1359
+
1360
+ ```tsx
1361
+ <p>
1362
+ <span className={styles["first-line"]}>The opening line</span>
1363
+ is styled differently from the rest.
1364
+ </p>
1365
+ ```
1366
+
1367
+ **Correct (native ::first-line):**
1368
+
1369
+ ```css
1370
+ .article p:first-of-type::first-line {
1371
+ font-variant-caps: small-caps;
1372
+ font-weight: var(--font-weight-medium);
1373
+ }
1374
+ ```
1375
+
1376
+ Reference: [MDN Pseudo-elements Reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-elements), [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)
1377
+
1378
+ ---
1379
+
1380
+ ## 5. Audio Feedback
1381
+
1382
+ **Impact:** MEDIUM — When and how to use sound in UI, covering accessibility, appropriateness, and implementation.
1383
+
1384
+ ### 5.1 Visual Equivalent for Every Sound
1385
+
1386
+ Every audio cue must have a visual equivalent; sound never replaces visual feedback.
1387
+
1388
+ **Incorrect (sound without visual):**
1389
+
1390
+ ```tsx
1391
+ function SubmitButton({ onClick }) {
1392
+ const handleClick = () => {
1393
+ playSound("success");
1394
+ onClick();
1395
+ };
1396
+ }
1397
+ ```
1398
+
1399
+ **Correct (sound with visual):**
1400
+
1401
+ ```tsx
1402
+ function SubmitButton({ onClick }) {
1403
+ const [status, setStatus] = useState("idle");
1404
+
1405
+ const handleClick = () => {
1406
+ playSound("success");
1407
+ setStatus("success");
1408
+ onClick();
1409
+ };
1410
+
1411
+ return <button data-status={status}>Submit</button>;
1412
+ }
1413
+ ```
1414
+
1415
+ ### 5.2 Toggle Setting to Disable Sounds
1416
+
1417
+ Provide explicit toggle to disable sounds in settings.
1418
+
1419
+ **Incorrect (no way to disable):**
1420
+
1421
+ ```tsx
1422
+ function App() {
1423
+ return <SoundProvider>{children}</SoundProvider>;
1424
+ }
1425
+ ```
1426
+
1427
+ **Correct (toggle available):**
1428
+
1429
+ ```tsx
1430
+ function App() {
1431
+ const { soundEnabled } = usePreferences();
1432
+ return (
1433
+ <SoundProvider enabled={soundEnabled}>
1434
+ {children}
1435
+ </SoundProvider>
1436
+ );
1437
+ }
1438
+ ```
1439
+
1440
+ ### 5.3 Respect prefers-reduced-motion for Sound
1441
+
1442
+ Respect prefers-reduced-motion as proxy for sound sensitivity.
1443
+
1444
+ **Incorrect (ignores preference):**
1445
+
1446
+ ```tsx
1447
+ function playSound(name: string) {
1448
+ audio.play();
1449
+ }
1450
+ ```
1451
+
1452
+ **Correct (checks preference):**
1453
+
1454
+ ```tsx
1455
+ function playSound(name: string) {
1456
+ const prefersReducedMotion = window.matchMedia(
1457
+ "(prefers-reduced-motion: reduce)"
1458
+ ).matches;
1459
+
1460
+ if (prefersReducedMotion) return;
1461
+ audio.play();
1462
+ }
1463
+ ```
1464
+
1465
+ ### 5.4 Independent Volume Control
1466
+
1467
+ Allow volume adjustment independent of system volume.
1468
+
1469
+ **Incorrect (always full volume):**
1470
+
1471
+ ```tsx
1472
+ function playSound() {
1473
+ audio.volume = 1;
1474
+ audio.play();
1475
+ }
1476
+ ```
1477
+
1478
+ **Correct (user-controlled volume):**
1479
+
1480
+ ```tsx
1481
+ function playSound() {
1482
+ const { volume } = usePreferences();
1483
+ audio.volume = volume;
1484
+ audio.play();
1485
+ }
1486
+ ```
1487
+
1488
+ ### 5.5 No Sound on High-Frequency Interactions
1489
+
1490
+ Do not add sound to high-frequency interactions (typing, keyboard navigation).
1491
+
1492
+ **Incorrect (sound on every keystroke):**
1493
+
1494
+ ```tsx
1495
+ function Input({ onChange }) {
1496
+ const handleChange = (e) => {
1497
+ playSound("keystroke");
1498
+ onChange(e);
1499
+ };
1500
+ }
1501
+ ```
1502
+
1503
+ **Correct (no sound on typing):**
1504
+
1505
+ ```tsx
1506
+ function Input({ onChange }) {
1507
+ return <input onChange={onChange} />;
1508
+ }
1509
+ ```
1510
+
1511
+ ### 5.6 Sound for Confirmations
1512
+
1513
+ Sound is appropriate for confirmations: payments, uploads, form submissions.
1514
+
1515
+ **Correct:**
1516
+
1517
+ ```tsx
1518
+ async function handlePayment() {
1519
+ await processPayment();
1520
+ playSound("success");
1521
+ showConfirmation();
1522
+ }
1523
+ ```
1524
+
1525
+ ### 5.7 Sound for Errors and Warnings
1526
+
1527
+ Sound is appropriate for errors and warnings that can't be overlooked.
1528
+
1529
+ **Correct:**
1530
+
1531
+ ```tsx
1532
+ function handleError(error: Error) {
1533
+ playSound("error");
1534
+ showErrorToast(error.message);
1535
+ }
1536
+ ```
1537
+
1538
+ ### 5.8 No Decorative Sound
1539
+
1540
+ Do not add sound to decorative moments with no informational value.
1541
+
1542
+ **Incorrect (hover sound):**
1543
+
1544
+ ```tsx
1545
+ function Card({ onHover }) {
1546
+ return (
1547
+ <div onMouseEnter={() => playSound("hover")}>
1548
+ {children}
1549
+ </div>
1550
+ );
1551
+ }
1552
+ ```
1553
+
1554
+ ### 5.9 Informative Not Punishing Sound
1555
+
1556
+ Sound should inform, not punish; avoid harsh sounds for user mistakes.
1557
+
1558
+ **Incorrect (harsh buzzer):**
1559
+
1560
+ ```tsx
1561
+ function ValidationError() {
1562
+ playSound("loud-buzzer");
1563
+ return <span>Invalid input</span>;
1564
+ }
1565
+ ```
1566
+
1567
+ **Correct (gentle alert):**
1568
+
1569
+ ```tsx
1570
+ function ValidationError() {
1571
+ playSound("gentle-alert");
1572
+ return <span>Invalid input</span>;
1573
+ }
1574
+ ```
1575
+
1576
+ ### 5.10 Preload Audio Files
1577
+
1578
+ Preload audio files to avoid playback delay.
1579
+
1580
+ **Incorrect (loads on demand):**
1581
+
1582
+ ```tsx
1583
+ function playSound(name: string) {
1584
+ const audio = new Audio(`/sounds/${name}.mp3`);
1585
+ audio.play();
1586
+ }
1587
+ ```
1588
+
1589
+ **Correct (preloaded):**
1590
+
1591
+ ```tsx
1592
+ const sounds = {
1593
+ success: new Audio("/sounds/success.mp3"),
1594
+ error: new Audio("/sounds/error.mp3"),
1595
+ };
1596
+
1597
+ Object.values(sounds).forEach(audio => audio.load());
1598
+
1599
+ function playSound(name: keyof typeof sounds) {
1600
+ sounds[name].currentTime = 0;
1601
+ sounds[name].play();
1602
+ }
1603
+ ```
1604
+
1605
+ ### 5.11 Subtle Default Volume
1606
+
1607
+ Default volume should be subtle, not loud.
1608
+
1609
+ **Incorrect (too loud):**
1610
+
1611
+ ```tsx
1612
+ const DEFAULT_VOLUME = 1.0;
1613
+ ```
1614
+
1615
+ **Correct (subtle):**
1616
+
1617
+ ```tsx
1618
+ const DEFAULT_VOLUME = 0.3;
1619
+ ```
1620
+
1621
+ ### 5.12 Reset currentTime Before Replay
1622
+
1623
+ Reset audio currentTime before replay to allow rapid triggering.
1624
+
1625
+ **Incorrect (won't replay if playing):**
1626
+
1627
+ ```tsx
1628
+ function playSound() {
1629
+ audio.play();
1630
+ }
1631
+ ```
1632
+
1633
+ **Correct (reset before play):**
1634
+
1635
+ ```tsx
1636
+ function playSound() {
1637
+ audio.currentTime = 0;
1638
+ audio.play();
1639
+ }
1640
+ ```
1641
+
1642
+ ### 5.13 Match Sound Weight to Action
1643
+
1644
+ Sound weight should match action importance.
1645
+
1646
+ **Incorrect (fanfare for toggle):**
1647
+
1648
+ ```tsx
1649
+ function handleToggle() {
1650
+ playSound("triumphant-fanfare");
1651
+ setEnabled(!enabled);
1652
+ }
1653
+ ```
1654
+
1655
+ **Correct (weight matches action):**
1656
+
1657
+ ```tsx
1658
+ function handleToggle() {
1659
+ playSound("soft-click");
1660
+ setEnabled(!enabled);
1661
+ }
1662
+
1663
+ function handlePurchase() {
1664
+ playSound("success-chime");
1665
+ completePurchase();
1666
+ }
1667
+ ```
1668
+
1669
+ ### 5.14 Sound Duration Matches Action Duration
1670
+
1671
+ Sound duration should match action duration.
1672
+
1673
+ **Incorrect (long sound for instant action):**
1674
+
1675
+ ```tsx
1676
+ function handleClick() {
1677
+ playSound("long-whoosh"); // 2000ms
1678
+ }
1679
+ ```
1680
+
1681
+ **Correct (matched duration):**
1682
+
1683
+ ```tsx
1684
+ function handleClick() {
1685
+ playSound("click"); // 50ms
1686
+ }
1687
+
1688
+ function handleUpload() {
1689
+ playSound("upload-progress"); // Matches upload duration
1690
+ }
1691
+ ```
1692
+
1693
+ **Sound appropriateness matrix:**
1694
+
1695
+ | Interaction | Sound? | Reason |
1696
+ |-------------|--------|--------|
1697
+ | Payment success | Yes | Significant confirmation |
1698
+ | Form submission | Yes | User needs assurance |
1699
+ | Error state | Yes | Can't be overlooked |
1700
+ | Notification | Yes | May not be looking at screen |
1701
+ | Button click | Maybe | Only for significant buttons |
1702
+ | Typing | No | Too frequent |
1703
+ | Hover | No | Decorative only |
1704
+ | Scroll | No | Too frequent |
1705
+ | Navigation | No | Keyboard nav would be noisy |
1706
+
1707
+ Reference: [Web Audio API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API), [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)
1708
+
1709
+ ---
1710
+
1711
+ ## 6. Sound Synthesis
1712
+
1713
+ **Impact:** MEDIUM — Web Audio API best practices for procedural sound generation.
1714
+
1715
+ ### 6.1 Reuse Single AudioContext
1716
+
1717
+ Reuse a single AudioContext instance; do not create new ones per sound.
1718
+
1719
+ **Incorrect (new context per call):**
1720
+
1721
+ ```ts
1722
+ function playSound() {
1723
+ const ctx = new AudioContext();
1724
+ }
1725
+ ```
1726
+
1727
+ **Correct (singleton):**
1728
+
1729
+ ```ts
1730
+ let audioContext: AudioContext | null = null;
1731
+
1732
+ function getAudioContext(): AudioContext {
1733
+ if (!audioContext) {
1734
+ audioContext = new AudioContext();
1735
+ }
1736
+ return audioContext;
1737
+ }
1738
+ ```
1739
+
1740
+ ### 6.2 Resume Suspended AudioContext
1741
+
1742
+ Check and resume suspended AudioContext before playing.
1743
+
1744
+ **Incorrect (plays without checking):**
1745
+
1746
+ ```ts
1747
+ function playSound() {
1748
+ const ctx = getAudioContext();
1749
+ }
1750
+ ```
1751
+
1752
+ **Correct (resumes if suspended):**
1753
+
1754
+ ```ts
1755
+ function playSound() {
1756
+ const ctx = getAudioContext();
1757
+ if (ctx.state === "suspended") {
1758
+ ctx.resume();
1759
+ }
1760
+ }
1761
+ ```
1762
+
1763
+ ### 6.3 Clean Up Audio Nodes After Playback
1764
+
1765
+ Disconnect and clean up audio nodes after playback.
1766
+
1767
+ **Incorrect (nodes remain connected):**
1768
+
1769
+ ```ts
1770
+ source.start();
1771
+ ```
1772
+
1773
+ **Correct (cleaned up on end):**
1774
+
1775
+ ```ts
1776
+ source.start();
1777
+ source.onended = () => {
1778
+ source.disconnect();
1779
+ gain.disconnect();
1780
+ };
1781
+ ```
1782
+
1783
+ ### 6.4 Exponential Decay for Natural Sound
1784
+
1785
+ Use exponential ramps for natural decay, not linear.
1786
+
1787
+ **Incorrect (linear ramp):**
1788
+
1789
+ ```ts
1790
+ gain.gain.linearRampToValueAtTime(0, t + 0.05);
1791
+ ```
1792
+
1793
+ **Correct (exponential ramp):**
1794
+
1795
+ ```ts
1796
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
1797
+ ```
1798
+
1799
+ ### 6.5 No Zero Target for Exponential Ramps
1800
+
1801
+ Exponential ramps cannot target 0; use 0.001 or similar small value.
1802
+
1803
+ **Incorrect (targets zero):**
1804
+
1805
+ ```ts
1806
+ gain.gain.exponentialRampToValueAtTime(0, t + 0.05);
1807
+ ```
1808
+
1809
+ **Correct (targets near-zero):**
1810
+
1811
+ ```ts
1812
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
1813
+ ```
1814
+
1815
+ ### 6.6 Set Initial Value Before Ramp
1816
+
1817
+ Set initial value before ramping to avoid glitches.
1818
+
1819
+ **Incorrect (no initial value):**
1820
+
1821
+ ```ts
1822
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
1823
+ ```
1824
+
1825
+ **Correct (initial value set):**
1826
+
1827
+ ```ts
1828
+ gain.gain.setValueAtTime(0.3, t);
1829
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
1830
+ ```
1831
+
1832
+ ### 6.7 Noise for Percussive Sounds
1833
+
1834
+ Use filtered noise for clicks/taps, not oscillators.
1835
+
1836
+ **Incorrect (oscillator for click):**
1837
+
1838
+ ```ts
1839
+ const osc = ctx.createOscillator();
1840
+ osc.type = "sine";
1841
+ ```
1842
+
1843
+ **Correct (noise burst for click):**
1844
+
1845
+ ```ts
1846
+ const buffer = ctx.createBuffer(1, ctx.sampleRate * 0.008, ctx.sampleRate);
1847
+ const data = buffer.getChannelData(0);
1848
+ for (let i = 0; i < data.length; i++) {
1849
+ data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 50);
1850
+ }
1851
+ ```
1852
+
1853
+ ### 6.8 Oscillators for Tonal Sounds
1854
+
1855
+ Use oscillators with pitch movement for tonal sounds (pops, confirmations).
1856
+
1857
+ **Incorrect (static frequency):**
1858
+
1859
+ ```ts
1860
+ osc.frequency.value = 400;
1861
+ ```
1862
+
1863
+ **Correct (pitch sweep):**
1864
+
1865
+ ```ts
1866
+ osc.frequency.setValueAtTime(400, t);
1867
+ osc.frequency.exponentialRampToValueAtTime(600, t + 0.04);
1868
+ ```
1869
+
1870
+ ### 6.9 Bandpass Filter for Sound Character
1871
+
1872
+ Apply bandpass filter to shape percussive sounds.
1873
+
1874
+ **Incorrect (raw noise):**
1875
+
1876
+ ```ts
1877
+ source.connect(gain).connect(ctx.destination);
1878
+ ```
1879
+
1880
+ **Correct (filtered noise):**
1881
+
1882
+ ```ts
1883
+ const filter = ctx.createBiquadFilter();
1884
+ filter.type = "bandpass";
1885
+ filter.frequency.value = 4000;
1886
+ filter.Q.value = 3;
1887
+ source.connect(filter).connect(gain).connect(ctx.destination);
1888
+ ```
1889
+
1890
+ ### 6.10 Click Duration 5-15ms
1891
+
1892
+ Click/tap sounds should be 5-15ms duration.
1893
+
1894
+ **Incorrect (too long):**
1895
+
1896
+ ```ts
1897
+ const buffer = ctx.createBuffer(1, ctx.sampleRate * 0.1, ctx.sampleRate);
1898
+ ```
1899
+
1900
+ **Correct (appropriate duration):**
1901
+
1902
+ ```ts
1903
+ const buffer = ctx.createBuffer(1, ctx.sampleRate * 0.008, ctx.sampleRate);
1904
+ ```
1905
+
1906
+ ### 6.11 Click Filter 3000-6000Hz
1907
+
1908
+ Bandpass filter for clicks should be 3000-6000Hz.
1909
+
1910
+ **Incorrect (too low):**
1911
+
1912
+ ```ts
1913
+ filter.frequency.value = 500;
1914
+ ```
1915
+
1916
+ **Correct (crisp range):**
1917
+
1918
+ ```ts
1919
+ filter.frequency.value = 4000;
1920
+ ```
1921
+
1922
+ ### 6.12 Gain Under 1.0
1923
+
1924
+ Gain values should not exceed 1.0 to prevent clipping.
1925
+
1926
+ **Incorrect (clipping):**
1927
+
1928
+ ```ts
1929
+ gain.gain.setValueAtTime(1.5, t);
1930
+ ```
1931
+
1932
+ **Correct (safe gain):**
1933
+
1934
+ ```ts
1935
+ gain.gain.setValueAtTime(0.3, t);
1936
+ ```
1937
+
1938
+ ### 6.13 Filter Q Value 2-5
1939
+
1940
+ Filter Q for clicks should be 2-5 for focused but not harsh sound.
1941
+
1942
+ **Incorrect (too resonant):**
1943
+
1944
+ ```ts
1945
+ filter.Q.value = 15;
1946
+ ```
1947
+
1948
+ **Correct (balanced Q):**
1949
+
1950
+ ```ts
1951
+ filter.Q.value = 3;
1952
+ ```
1953
+
1954
+ **Parameter translation table:**
1955
+
1956
+ | User Says | Parameter Change |
1957
+ |-----------|------------------|
1958
+ | "too harsh" | Lower filter frequency, reduce Q |
1959
+ | "too muffled" | Higher filter frequency |
1960
+ | "too long" | Shorter duration, faster decay |
1961
+ | "cuts off abruptly" | Use exponential decay |
1962
+ | "more mechanical" | Higher Q, faster decay |
1963
+ | "softer" | Lower gain, triangle wave |
1964
+
1965
+ Reference: [Web Audio API - MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
1966
+
1967
+ ---
1968
+
1969
+ ## 7. Morphing Icons
1970
+
1971
+ **Impact:** LOW — Building icon components that morph between any two icons through SVG line transformation.
1972
+
1973
+ **Core concept:** Every icon is composed of exactly three SVG lines. Icons that need fewer lines collapse the extras to invisible center points. This constraint enables seamless morphing between any two icons.
1974
+
1975
+ **Architecture:**
1976
+
1977
+ ```ts
1978
+ interface IconLine {
1979
+ x1: number;
1980
+ y1: number;
1981
+ x2: number;
1982
+ y2: number;
1983
+ opacity?: number;
1984
+ }
1985
+
1986
+ interface IconDefinition {
1987
+ lines: [IconLine, IconLine, IconLine];
1988
+ rotation?: number;
1989
+ group?: string;
1990
+ }
1991
+
1992
+ const CENTER = 7;
1993
+ const collapsed: IconLine = {
1994
+ x1: CENTER, y1: CENTER, x2: CENTER, y2: CENTER, opacity: 0,
1995
+ };
1996
+ ```
1997
+
1998
+ ### 7.1 Icons Must Use Exactly Three Lines
1999
+
2000
+ Every icon MUST use exactly 3 lines. No more, no fewer.
2001
+
2002
+ **Incorrect (only 2 lines):**
2003
+
2004
+ ```ts
2005
+ const checkIcon = {
2006
+ lines: [
2007
+ { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
2008
+ { x1: 5.5, y1: 11, x2: 12, y2: 3 },
2009
+ ],
2010
+ };
2011
+ ```
2012
+
2013
+ **Correct (3 lines with collapsed):**
2014
+
2015
+ ```ts
2016
+ const checkIcon = {
2017
+ lines: [
2018
+ { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
2019
+ { x1: 5.5, y1: 11, x2: 12, y2: 3 },
2020
+ collapsed,
2021
+ ],
2022
+ };
2023
+ ```
2024
+
2025
+ ### 7.2 Use Collapsed Constant for Unused Lines
2026
+
2027
+ Unused lines must use the collapsed constant, not omission or null.
2028
+
2029
+ **Incorrect (null for unused):**
2030
+
2031
+ ```ts
2032
+ const minusIcon = {
2033
+ lines: [
2034
+ { x1: 2, y1: 7, x2: 12, y2: 7 },
2035
+ null,
2036
+ null,
2037
+ ],
2038
+ };
2039
+ ```
2040
+
2041
+ **Correct (collapsed constant):**
2042
+
2043
+ ```ts
2044
+ const minusIcon = {
2045
+ lines: [
2046
+ { x1: 2, y1: 7, x2: 12, y2: 7 },
2047
+ collapsed,
2048
+ collapsed,
2049
+ ],
2050
+ };
2051
+ ```
2052
+
2053
+ ### 7.3 Consistent ViewBox Size
2054
+
2055
+ All icons must use the same viewBox (14x14 recommended).
2056
+
2057
+ **Incorrect (mixed scales):**
2058
+
2059
+ ```ts
2060
+ const icon1 = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] }; // 14x14
2061
+ const icon2 = { lines: [{ x1: 4, y1: 14, x2: 24, y2: 14 }, ...] }; // 28x28
2062
+ ```
2063
+
2064
+ **Correct (consistent scale):**
2065
+
2066
+ ```ts
2067
+ const VIEWBOX_SIZE = 14;
2068
+ const CENTER = 7;
2069
+ ```
2070
+
2071
+ ### 7.4 Shared Group for Rotational Variants
2072
+
2073
+ Icons that are rotational variants MUST share the same group and base lines.
2074
+
2075
+ **Incorrect (different line definitions):**
2076
+
2077
+ ```ts
2078
+ const arrowRight = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] };
2079
+ const arrowDown = { lines: [{ x1: 7, y1: 2, x2: 7, y2: 12 }, ...] };
2080
+ ```
2081
+
2082
+ **Correct (shared base lines):**
2083
+
2084
+ ```ts
2085
+ const arrowLines: [IconLine, IconLine, IconLine] = [
2086
+ { x1: 2, y1: 7, x2: 12, y2: 7 },
2087
+ { x1: 7.5, y1: 2.5, x2: 12, y2: 7 },
2088
+ { x1: 7.5, y1: 11.5, x2: 12, y2: 7 },
2089
+ ];
2090
+
2091
+ const icons = {
2092
+ "arrow-right": { lines: arrowLines, rotation: 0, group: "arrow" },
2093
+ "arrow-down": { lines: arrowLines, rotation: 90, group: "arrow" },
2094
+ "arrow-left": { lines: arrowLines, rotation: 180, group: "arrow" },
2095
+ "arrow-up": { lines: arrowLines, rotation: -90, group: "arrow" },
2096
+ };
2097
+ ```
2098
+
2099
+ ### 7.5 Spring Physics for Rotation
2100
+
2101
+ Rotation between grouped icons should use spring physics for natural motion.
2102
+
2103
+ **Incorrect (duration-based rotation):**
2104
+
2105
+ ```tsx
2106
+ <motion.g animate={{ rotate: rotation }} transition={{ duration: 0.3 }} />
2107
+ ```
2108
+
2109
+ **Correct (spring rotation):**
2110
+
2111
+ ```tsx
2112
+ const rotation = useSpring(definition.rotation ?? 0, activeTransition);
2113
+
2114
+ <motion.g style={{ rotate: rotation, transformOrigin: "center" }} />
2115
+ ```
2116
+
2117
+ ### 7.6 Reduced Motion Support for Icons
2118
+
2119
+ Respect prefers-reduced-motion by disabling animations.
2120
+
2121
+ **Incorrect (always animates):**
2122
+
2123
+ ```tsx
2124
+ function MorphingIcon({ icon }: Props) {
2125
+ return <motion.line animate={...} transition={{ duration: 0.4 }} />;
2126
+ }
2127
+ ```
2128
+
2129
+ **Correct (respects preference):**
2130
+
2131
+ ```tsx
2132
+ function MorphingIcon({ icon }: Props) {
2133
+ const reducedMotion = useReducedMotion() ?? false;
2134
+ const activeTransition = reducedMotion ? { duration: 0 } : transition;
2135
+
2136
+ return <motion.line animate={...} transition={activeTransition} />;
2137
+ }
2138
+ ```
2139
+
2140
+ ### 7.7 Instant Jump for Non-Grouped Icons
2141
+
2142
+ When transitioning between icons NOT in the same group, rotation should jump instantly.
2143
+
2144
+ **Incorrect (always animates rotation):**
2145
+
2146
+ ```tsx
2147
+ useEffect(() => {
2148
+ rotation.set(definition.rotation ?? 0);
2149
+ }, [definition]);
2150
+ ```
2151
+
2152
+ **Correct (jumps when not grouped):**
2153
+
2154
+ ```tsx
2155
+ useEffect(() => {
2156
+ if (shouldRotate) {
2157
+ rotation.set(definition.rotation ?? 0);
2158
+ } else {
2159
+ rotation.jump(definition.rotation ?? 0);
2160
+ }
2161
+ }, [definition, shouldRotate]);
2162
+ ```
2163
+
2164
+ ### 7.8 Round Stroke Line Caps
2165
+
2166
+ Lines should use strokeLinecap="round" for polished endpoints.
2167
+
2168
+ **Incorrect (butt caps):**
2169
+
2170
+ ```tsx
2171
+ <motion.line strokeLinecap="butt" />
2172
+ ```
2173
+
2174
+ **Correct (round caps):**
2175
+
2176
+ ```tsx
2177
+ <motion.line strokeLinecap="round" />
2178
+ ```
2179
+
2180
+ ### 7.9 Aria Hidden on Icon SVGs
2181
+
2182
+ Icon SVGs should be aria-hidden since they're decorative.
2183
+
2184
+ **Incorrect (no aria attribute):**
2185
+
2186
+ ```tsx
2187
+ <svg width={size} height={size}>...</svg>
2188
+ ```
2189
+
2190
+ **Correct (aria-hidden):**
2191
+
2192
+ ```tsx
2193
+ <svg width={size} height={size} aria-hidden="true">...</svg>
2194
+ ```
2195
+
2196
+ **Common icon patterns:**
2197
+
2198
+ ```ts
2199
+ // Two-line icons (check, minus, chevron) — one collapsed line
2200
+ const check = {
2201
+ lines: [
2202
+ { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
2203
+ { x1: 5.5, y1: 11, x2: 12, y2: 3 },
2204
+ collapsed,
2205
+ ],
2206
+ };
2207
+
2208
+ // Three-line icons (menu, asterisk) — all lines used
2209
+ const menu = {
2210
+ lines: [
2211
+ { x1: 2, y1: 3.5, x2: 12, y2: 3.5 },
2212
+ { x1: 2, y1: 7, x2: 12, y2: 7 },
2213
+ { x1: 2, y1: 10.5, x2: 12, y2: 10.5 },
2214
+ ],
2215
+ };
2216
+
2217
+ // Point icons (more, grip) — zero-length lines as dots
2218
+ const more = {
2219
+ lines: [
2220
+ { x1: 3, y1: 7, x2: 3, y2: 7 },
2221
+ { x1: 7, y1: 7, x2: 7, y2: 7 },
2222
+ { x1: 11, y1: 7, x2: 11, y2: 7 },
2223
+ ],
2224
+ };
2225
+ ```
2226
+
2227
+ **Recommended transition:**
2228
+
2229
+ ```ts
2230
+ const defaultTransition: Transition = {
2231
+ ease: [0.19, 1, 0.22, 1],
2232
+ duration: 0.4,
2233
+ };
2234
+ ```
2235
+
2236
+ Reference: [Motion useSpring](https://motion.dev/docs/react-use-spring), [SVG Line Element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
2237
+
2238
+ ---
2239
+
2240
+ ## 8. Container Animation
2241
+
2242
+ **Impact:** MEDIUM — Animating container width and height using a measure-and-animate pattern with ResizeObserver and Motion.
2243
+
2244
+ ### 8.1 Two-Div Pattern for Animated Bounds
2245
+
2246
+ Use an outer animated div and an inner measured div. Never measure and animate the same element — it creates a feedback loop.
2247
+
2248
+ **Incorrect (measure and animate same element):**
2249
+
2250
+ ```tsx
2251
+ function AnimatedContainer({ children }) {
2252
+ const [ref, bounds] = useMeasure();
2253
+ return (
2254
+ <motion.div ref={ref} animate={{ height: bounds.height }}>
2255
+ {children}
2256
+ </motion.div>
2257
+ );
2258
+ }
2259
+ ```
2260
+
2261
+ **Correct (separate measure and animate targets):**
2262
+
2263
+ ```tsx
2264
+ function AnimatedContainer({ children }) {
2265
+ const [ref, bounds] = useMeasure();
2266
+ return (
2267
+ <motion.div animate={{ height: bounds.height }}>
2268
+ <div ref={ref}>{children}</div>
2269
+ </motion.div>
2270
+ );
2271
+ }
2272
+ ```
2273
+
2274
+ ### 8.2 Guard Against Zero on Initial Render
2275
+
2276
+ On initial render, measured bounds are 0. Guard against this to prevent animating from 0 to actual size.
2277
+
2278
+ **Incorrect (animates from 0 on mount):**
2279
+
2280
+ ```tsx
2281
+ <motion.div animate={{ width: bounds.width }}>
2282
+ <div ref={ref}>{children}</div>
2283
+ </motion.div>
2284
+ ```
2285
+
2286
+ **Correct (falls back to auto on first frame):**
2287
+
2288
+ ```tsx
2289
+ <motion.div animate={{ width: bounds.width > 0 ? bounds.width : "auto" }}>
2290
+ <div ref={ref}>{children}</div>
2291
+ </motion.div>
2292
+ ```
2293
+
2294
+ ### 8.3 Use ResizeObserver for Measurement
2295
+
2296
+ Use ResizeObserver to track element dimensions. It fires on resize without causing layout thrashing.
2297
+
2298
+ **Incorrect (measuring on every render):**
2299
+
2300
+ ```tsx
2301
+ function useMeasure(ref) {
2302
+ const [bounds, setBounds] = useState({ width: 0, height: 0 });
2303
+ useEffect(() => {
2304
+ if (ref.current) {
2305
+ const rect = ref.current.getBoundingClientRect();
2306
+ setBounds({ width: rect.width, height: rect.height });
2307
+ }
2308
+ });
2309
+ return bounds;
2310
+ }
2311
+ ```
2312
+
2313
+ **Correct (ResizeObserver):**
2314
+
2315
+ ```tsx
2316
+ function useMeasure() {
2317
+ const [element, setElement] = useState(null);
2318
+ const [bounds, setBounds] = useState({ width: 0, height: 0 });
2319
+ const ref = useCallback((node) => setElement(node), []);
2320
+
2321
+ useEffect(() => {
2322
+ if (!element) return;
2323
+ const observer = new ResizeObserver(([entry]) => {
2324
+ setBounds({
2325
+ width: entry.contentRect.width,
2326
+ height: entry.contentRect.height,
2327
+ });
2328
+ });
2329
+ observer.observe(element);
2330
+ return () => observer.disconnect();
2331
+ }, [element]);
2332
+
2333
+ return [ref, bounds];
2334
+ }
2335
+ ```
2336
+
2337
+ ### 8.4 Overflow Hidden on Animated Container
2338
+
2339
+ Set overflow: hidden on the animated outer container to clip content during size transitions.
2340
+
2341
+ **Incorrect (content overflows during animation):**
2342
+
2343
+ ```tsx
2344
+ <motion.div animate={{ height: bounds.height }}>
2345
+ <div ref={ref}>{children}</div>
2346
+ </motion.div>
2347
+ ```
2348
+
2349
+ **Correct (clipped during transition):**
2350
+
2351
+ ```tsx
2352
+ <motion.div animate={{ height: bounds.height }} style={{ overflow: "hidden" }}>
2353
+ <div ref={ref}>{children}</div>
2354
+ </motion.div>
2355
+ ```
2356
+
2357
+ ### 8.5 Use Animated Bounds Sparingly
2358
+
2359
+ Animated bounds is a subtle effect. Reserve it for interactive elements where size changes are meaningful.
2360
+
2361
+ **Good use cases:** loading state buttons, expandable sections, accordions, FAQs, content reveals.
2362
+
2363
+ **Bad use cases:** every container on the page, static layouts, elements that don't change size.
2364
+
2365
+ ### 8.6 Use Callback Ref for Measurement
2366
+
2367
+ Use a callback ref (not useRef) for measurement hooks so the observer attaches when the DOM node is ready.
2368
+
2369
+ **Incorrect (useRef may be null on first effect):**
2370
+
2371
+ ```tsx
2372
+ const ref = useRef(null);
2373
+ useEffect(() => {
2374
+ if (!ref.current) return;
2375
+ observer.observe(ref.current);
2376
+ }, []);
2377
+ ```
2378
+
2379
+ **Correct (callback ref guarantees node):**
2380
+
2381
+ ```tsx
2382
+ const [element, setElement] = useState(null);
2383
+ const ref = useCallback((node) => setElement(node), []);
2384
+ useEffect(() => {
2385
+ if (!element) return;
2386
+ observer.observe(element);
2387
+ return () => observer.disconnect();
2388
+ }, [element]);
2389
+ ```
2390
+
2391
+ ### 8.7 Add Delay for Natural Container Transitions
2392
+
2393
+ Add a small delay so the transition feels like it's catching up to the content.
2394
+
2395
+ **Correct:**
2396
+
2397
+ ```tsx
2398
+ <motion.div
2399
+ animate={{ height: bounds.height }}
2400
+ transition={{ duration: 0.2, delay: 0.05 }}
2401
+ style={{ overflow: "hidden" }}
2402
+ >
2403
+ <div ref={ref}>{children}</div>
2404
+ </motion.div>
2405
+ ```
2406
+
2407
+ Reference: [ResizeObserver - MDN](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver), [Motion Documentation](https://motion.dev)
2408
+
2409
+ ---
2410
+
2411
+ ## 9. Laws of UX
2412
+
2413
+ **Impact:** HIGH — Psychological principles behind interfaces that feel right. Violating these creates friction users can't articulate.
2414
+
2415
+ ### 9.1 Size Interactive Targets for Easy Clicking
2416
+
2417
+ The bigger something is, the easier it is to click (Fitts's Law). Make interactive elements at least 32px.
2418
+
2419
+ **Incorrect (tiny click target):**
2420
+
2421
+ ```css
2422
+ .icon-button {
2423
+ width: 16px;
2424
+ height: 16px;
2425
+ padding: 0;
2426
+ }
2427
+ ```
2428
+
2429
+ **Correct (comfortable target):**
2430
+
2431
+ ```css
2432
+ .icon-button {
2433
+ width: 32px;
2434
+ height: 32px;
2435
+ padding: 8px;
2436
+ }
2437
+ ```
2438
+
2439
+ ### 9.2 Expand Hit Areas with Invisible Padding
2440
+
2441
+ Use pseudo-elements or invisible padding to expand clickable areas beyond visible bounds.
2442
+
2443
+ **Incorrect (visible size equals hit area):**
2444
+
2445
+ ```css
2446
+ .link {
2447
+ font-size: 14px;
2448
+ }
2449
+ ```
2450
+
2451
+ **Correct (expanded invisible hit area):**
2452
+
2453
+ ```css
2454
+ .link {
2455
+ position: relative;
2456
+ }
2457
+
2458
+ .link::before {
2459
+ content: "";
2460
+ position: absolute;
2461
+ inset: -8px -12px;
2462
+ }
2463
+ ```
2464
+
2465
+ ### 9.3 Minimize Choices to Reduce Decision Time
2466
+
2467
+ Decision time increases logarithmically with the number of choices (Hick's Law). Use progressive disclosure.
2468
+
2469
+ **Incorrect (all options at once):**
2470
+
2471
+ ```tsx
2472
+ function Settings() {
2473
+ return (
2474
+ <div>
2475
+ {allSettings.map(setting => (
2476
+ <SettingRow key={setting.id} {...setting} />
2477
+ ))}
2478
+ </div>
2479
+ );
2480
+ }
2481
+ ```
2482
+
2483
+ **Correct (progressive disclosure):**
2484
+
2485
+ ```tsx
2486
+ function Settings() {
2487
+ return (
2488
+ <div>
2489
+ {commonSettings.map(setting => (
2490
+ <SettingRow key={setting.id} {...setting} />
2491
+ ))}
2492
+ <details>
2493
+ <summary>Advanced</summary>
2494
+ {advancedSettings.map(setting => (
2495
+ <SettingRow key={setting.id} {...setting} />
2496
+ ))}
2497
+ </details>
2498
+ </div>
2499
+ );
2500
+ }
2501
+ ```
2502
+
2503
+ ### 9.4 Chunk Data into Groups of 5-9
2504
+
2505
+ Working memory holds about 7 items (Miller's Law). Group and chunk large data sets for scannability.
2506
+
2507
+ **Incorrect (raw unformatted data):**
2508
+
2509
+ ```tsx
2510
+ <span>4532015112830366</span>
2511
+ ```
2512
+
2513
+ **Correct (chunked for readability):**
2514
+
2515
+ ```tsx
2516
+ <span>4532 0151 1283 0366</span>
2517
+ ```
2518
+
2519
+ ### 9.5 Respond Within 400ms
2520
+
2521
+ Interactions must respond within 400ms to feel instant (Doherty Threshold). Above this, users notice delay.
2522
+
2523
+ **Incorrect (no feedback during loading):**
2524
+
2525
+ ```tsx
2526
+ async function handleClick() {
2527
+ const data = await fetchData();
2528
+ setResult(data);
2529
+ }
2530
+ ```
2531
+
2532
+ **Correct (immediate optimistic feedback):**
2533
+
2534
+ ```tsx
2535
+ async function handleClick() {
2536
+ setResult(optimisticData);
2537
+ const data = await fetchData();
2538
+ setResult(data);
2539
+ }
2540
+ ```
2541
+
2542
+ ### 9.6 Fake Speed When Actual Speed Isn't Possible
2543
+
2544
+ If you can't make something fast, make it feel fast with optimistic UI, skeletons, or progress indicators.
2545
+
2546
+ **Incorrect (blank screen during load):**
2547
+
2548
+ ```tsx
2549
+ function Page() {
2550
+ const { data, isLoading } = useFetch("/api/data");
2551
+ if (isLoading) return null;
2552
+ return <Content data={data} />;
2553
+ }
2554
+ ```
2555
+
2556
+ **Correct (skeleton during load):**
2557
+
2558
+ ```tsx
2559
+ function Page() {
2560
+ const { data, isLoading } = useFetch("/api/data");
2561
+ if (isLoading) return <Skeleton />;
2562
+ return <Content data={data} />;
2563
+ }
2564
+ ```
2565
+
2566
+ ### 9.7 Accept Messy Input, Output Clean Data
2567
+
2568
+ Inputs should accept messy human data and normalize it (Postel's Law). Validate generously, format strictly.
2569
+
2570
+ **Incorrect (rigid format required):**
2571
+
2572
+ ```tsx
2573
+ function DateInput({ onChange }) {
2574
+ return (
2575
+ <input
2576
+ type="text"
2577
+ placeholder="YYYY-MM-DD"
2578
+ pattern="\d{4}-\d{2}-\d{2}"
2579
+ onChange={onChange}
2580
+ />
2581
+ );
2582
+ }
2583
+ ```
2584
+
2585
+ **Correct (accepts multiple formats):**
2586
+
2587
+ ```tsx
2588
+ function DateInput({ onChange }) {
2589
+ function handleChange(e) {
2590
+ const parsed = parseFlexibleDate(e.target.value);
2591
+ if (parsed) onChange(parsed);
2592
+ }
2593
+
2594
+ return (
2595
+ <input
2596
+ type="text"
2597
+ placeholder="Any date format"
2598
+ onChange={handleChange}
2599
+ />
2600
+ );
2601
+ }
2602
+ ```
2603
+
2604
+ ### 9.8 Show What Matters Now, Reveal Complexity Later
2605
+
2606
+ Don't overwhelm users with everything at once. Reveal complexity incrementally as needed.
2607
+
2608
+ **Incorrect (all controls visible):**
2609
+
2610
+ ```tsx
2611
+ function Editor() {
2612
+ return (
2613
+ <div>
2614
+ <BasicTools />
2615
+ <AdvancedTools />
2616
+ <ExpertTools />
2617
+ <DebugTools />
2618
+ </div>
2619
+ );
2620
+ }
2621
+ ```
2622
+
2623
+ **Correct (progressive disclosure):**
2624
+
2625
+ ```tsx
2626
+ function Editor() {
2627
+ const [showAdvanced, setShowAdvanced] = useState(false);
2628
+ return (
2629
+ <div>
2630
+ <BasicTools />
2631
+ {showAdvanced && <AdvancedTools />}
2632
+ <button onClick={() => setShowAdvanced(!showAdvanced)}>
2633
+ Toggle
2634
+ </button>
2635
+ </div>
2636
+ );
2637
+ }
2638
+ ```
2639
+
2640
+ ### 9.9 Use Familiar UI Patterns
2641
+
2642
+ Users spend most of their time on other sites. They expect yours to work the same way (Jakob's Law).
2643
+
2644
+ **Incorrect (custom unconventional navigation):**
2645
+
2646
+ ```tsx
2647
+ function Nav() {
2648
+ return (
2649
+ <nav>
2650
+ <button onClick={() => navigate("/")}>⬡</button>
2651
+ <button onClick={() => navigate("/search")}>⬢</button>
2652
+ </nav>
2653
+ );
2654
+ }
2655
+ ```
2656
+
2657
+ **Correct (standard recognizable patterns):**
2658
+
2659
+ ```tsx
2660
+ function Nav() {
2661
+ return (
2662
+ <nav>
2663
+ <Link href="/">Home</Link>
2664
+ <Link href="/search">Search</Link>
2665
+ </nav>
2666
+ );
2667
+ }
2668
+ ```
2669
+
2670
+ ### 9.10 Visual Polish Increases Perceived Usability
2671
+
2672
+ Users perceive aesthetically pleasing design as more usable. Small visual details compound into trust.
2673
+
2674
+ **Incorrect (unstyled, raw elements):**
2675
+
2676
+ ```css
2677
+ .card {
2678
+ border: 1px solid black;
2679
+ padding: 10px;
2680
+ }
2681
+ ```
2682
+
2683
+ **Correct (considered visual treatment):**
2684
+
2685
+ ```css
2686
+ .card {
2687
+ padding: 16px;
2688
+ background: var(--gray-2);
2689
+ border: 1px solid var(--gray-a4);
2690
+ border-radius: 12px;
2691
+ box-shadow: var(--shadow-1);
2692
+ }
2693
+ ```
2694
+
2695
+ ### 9.11 Group Related Elements Spatially
2696
+
2697
+ Elements near each other are perceived as related (Law of Proximity). Use spacing to create visual groups.
2698
+
2699
+ **Incorrect (uniform spacing between unrelated items):**
2700
+
2701
+ ```css
2702
+ .form label,
2703
+ .form input,
2704
+ .form .hint,
2705
+ .form .divider {
2706
+ margin-bottom: 16px;
2707
+ }
2708
+ ```
2709
+
2710
+ **Correct (tighter spacing within groups, larger between):**
2711
+
2712
+ ```css
2713
+ .form label {
2714
+ margin-bottom: 4px;
2715
+ }
2716
+
2717
+ .form input {
2718
+ margin-bottom: 2px;
2719
+ }
2720
+
2721
+ .form .hint {
2722
+ margin-bottom: 24px;
2723
+ }
2724
+ ```
2725
+
2726
+ ### 9.12 Similar Elements Should Look Alike
2727
+
2728
+ Elements that function the same should look the same (Law of Similarity). Visual consistency signals functional consistency.
2729
+
2730
+ **Incorrect (same function, different appearance):**
2731
+
2732
+ ```css
2733
+ .save-button {
2734
+ background: blue;
2735
+ border-radius: 8px;
2736
+ }
2737
+
2738
+ .submit-button {
2739
+ background: green;
2740
+ border-radius: 0;
2741
+ }
2742
+ ```
2743
+
2744
+ **Correct (same function, same appearance):**
2745
+
2746
+ ```css
2747
+ .primary-action {
2748
+ background: var(--gray-12);
2749
+ color: var(--gray-1);
2750
+ border-radius: 8px;
2751
+ }
2752
+ ```
2753
+
2754
+ ### 9.13 Use Boundaries to Group Related Content
2755
+
2756
+ Elements sharing a clearly defined boundary are perceived as a group (Law of Common Region).
2757
+
2758
+ **Incorrect (flat list with no visual grouping):**
2759
+
2760
+ ```tsx
2761
+ function Settings() {
2762
+ return (
2763
+ <div>
2764
+ <Toggle label="Dark mode" />
2765
+ <Toggle label="Notifications" />
2766
+ <Input label="Email" />
2767
+ <Input label="Password" />
2768
+ </div>
2769
+ );
2770
+ }
2771
+ ```
2772
+
2773
+ **Correct (bounded sections):**
2774
+
2775
+ ```tsx
2776
+ function Settings() {
2777
+ return (
2778
+ <div>
2779
+ <section className={styles.group}>
2780
+ <h3>Appearance</h3>
2781
+ <Toggle label="Dark mode" />
2782
+ </section>
2783
+ <section className={styles.group}>
2784
+ <h3>Account</h3>
2785
+ <Input label="Email" />
2786
+ <Input label="Password" />
2787
+ </section>
2788
+ </div>
2789
+ );
2790
+ }
2791
+ ```
2792
+
2793
+ ### 9.14 Make Important Elements Visually Distinct
2794
+
2795
+ When multiple similar elements are present, the one that differs is most likely to be remembered (Von Restorff Effect).
2796
+
2797
+ **Incorrect (primary action blends in):**
2798
+
2799
+ ```tsx
2800
+ <div className={styles.actions}>
2801
+ <button className={styles.button}>Cancel</button>
2802
+ <button className={styles.button}>Delete Account</button>
2803
+ </div>
2804
+ ```
2805
+
2806
+ **Correct (destructive action stands out):**
2807
+
2808
+ ```tsx
2809
+ <div className={styles.actions}>
2810
+ <button className={styles["button-secondary"]}>Cancel</button>
2811
+ <button className={styles["button-danger"]}>Delete Account</button>
2812
+ </div>
2813
+ ```
2814
+
2815
+ ### 9.15 Place Key Items First or Last
2816
+
2817
+ Users best remember the first and last items in a sequence (Serial Position Effect).
2818
+
2819
+ **Incorrect (important action buried in middle):**
2820
+
2821
+ ```tsx
2822
+ <nav>
2823
+ <Link href="/settings">Settings</Link>
2824
+ <Link href="/">Home</Link>
2825
+ <Link href="/about">About</Link>
2826
+ </nav>
2827
+ ```
2828
+
2829
+ **Correct (key items at edges):**
2830
+
2831
+ ```tsx
2832
+ <nav>
2833
+ <Link href="/">Home</Link>
2834
+ <Link href="/about">About</Link>
2835
+ <Link href="/settings">Settings</Link>
2836
+ </nav>
2837
+ ```
2838
+
2839
+ ### 9.16 End Experiences with Clear Success States
2840
+
2841
+ People judge experiences by their peak moment and their end (Peak-End Rule). Invest in completion states.
2842
+
2843
+ **Incorrect (abrupt end after action):**
2844
+
2845
+ ```tsx
2846
+ async function handleSubmit() {
2847
+ await submitForm(data);
2848
+ router.push("/");
2849
+ }
2850
+ ```
2851
+
2852
+ **Correct (satisfying completion state):**
2853
+
2854
+ ```tsx
2855
+ async function handleSubmit() {
2856
+ await submitForm(data);
2857
+ setStatus("success");
2858
+ }
2859
+
2860
+ return status === "success" ? (
2861
+ <SuccessScreen message="You're all set." />
2862
+ ) : (
2863
+ <Form onSubmit={handleSubmit} />
2864
+ );
2865
+ ```
2866
+
2867
+ ### 9.17 Move Complexity to the System
2868
+
2869
+ Every system has irreducible complexity (Tesler's Law). The question is who handles it — the user or the system.
2870
+
2871
+ **Incorrect (complexity pushed to user):**
2872
+
2873
+ ```tsx
2874
+ <input
2875
+ type="text"
2876
+ placeholder="Enter date as YYYY-MM-DDTHH:mm:ss.sssZ"
2877
+ />
2878
+ ```
2879
+
2880
+ **Correct (system absorbs complexity):**
2881
+
2882
+ ```tsx
2883
+ <DatePicker
2884
+ onChange={(date) => setDate(date.toISOString())}
2885
+ />
2886
+ ```
2887
+
2888
+ ### 9.18 Show Progress Toward Completion
2889
+
2890
+ People accelerate behavior as they approach a goal (Goal-Gradient Effect). Show how close they are.
2891
+
2892
+ **Incorrect (no sense of progress):**
2893
+
2894
+ ```tsx
2895
+ function Onboarding({ step }) {
2896
+ return <OnboardingStep step={step} />;
2897
+ }
2898
+ ```
2899
+
2900
+ **Correct (progress visible):**
2901
+
2902
+ ```tsx
2903
+ function Onboarding({ step, totalSteps }) {
2904
+ return (
2905
+ <div>
2906
+ <ProgressBar value={step} max={totalSteps} />
2907
+ <span>Step {step} of {totalSteps}</span>
2908
+ <OnboardingStep step={step} />
2909
+ </div>
2910
+ );
2911
+ }
2912
+ ```
2913
+
2914
+ ### 9.19 Show Incomplete State to Drive Completion
2915
+
2916
+ People remember incomplete tasks better than completed ones (Zeigarnik Effect).
2917
+
2918
+ **Incorrect (no indication of incomplete profile):**
2919
+
2920
+ ```tsx
2921
+ function Dashboard() {
2922
+ return <DashboardContent />;
2923
+ }
2924
+ ```
2925
+
2926
+ **Correct (incomplete state visible):**
2927
+
2928
+ ```tsx
2929
+ function Dashboard({ profile }) {
2930
+ return (
2931
+ <div>
2932
+ {!profile.isComplete && (
2933
+ <Banner>
2934
+ Complete your profile — {profile.completionPercent}% done
2935
+ </Banner>
2936
+ )}
2937
+ <DashboardContent />
2938
+ </div>
2939
+ );
2940
+ }
2941
+ ```
2942
+
2943
+ ### 9.20 Simplify Complex Visuals into Clear Forms
2944
+
2945
+ People interpret complex visuals as the simplest form possible (Law of Pragnanz). Reduce visual noise.
2946
+
2947
+ **Incorrect (visually noisy layout):**
2948
+
2949
+ ```css
2950
+ .card {
2951
+ border: 2px dashed red;
2952
+ background: linear-gradient(45deg, #f0f, #0ff);
2953
+ box-shadow: 5px 5px 0 black, 10px 10px 0 gray;
2954
+ outline: 3px dotted blue;
2955
+ }
2956
+ ```
2957
+
2958
+ **Correct (clear, simple form):**
2959
+
2960
+ ```css
2961
+ .card {
2962
+ background: var(--gray-2);
2963
+ border: 1px solid var(--gray-a4);
2964
+ border-radius: 12px;
2965
+ box-shadow: var(--shadow-1);
2966
+ }
2967
+ ```
2968
+
2969
+ ### 9.21 Prioritize the Critical 20% of Features
2970
+
2971
+ 80% of users use 20% of features (Pareto Principle). Optimize the critical path first.
2972
+
2973
+ **Incorrect (all features equally prominent):**
2974
+
2975
+ ```tsx
2976
+ function Toolbar() {
2977
+ return (
2978
+ <div>
2979
+ {allFeatures.map(f => <Button key={f.id}>{f.label}</Button>)}
2980
+ </div>
2981
+ );
2982
+ }
2983
+ ```
2984
+
2985
+ **Correct (critical features prominent, rest accessible):**
2986
+
2987
+ ```tsx
2988
+ function Toolbar() {
2989
+ return (
2990
+ <div>
2991
+ {criticalFeatures.map(f => <Button key={f.id}>{f.label}</Button>)}
2992
+ <MoreMenu features={secondaryFeatures} />
2993
+ </div>
2994
+ );
2995
+ }
2996
+ ```
2997
+
2998
+ ### 9.22 Minimize Extraneous Cognitive Load
2999
+
3000
+ Remove anything that doesn't help the user complete their task. Decoration, redundant labels, and unnecessary options all add load.
3001
+
3002
+ **Incorrect (extraneous elements):**
3003
+
3004
+ ```tsx
3005
+ function DeleteDialog() {
3006
+ return (
3007
+ <dialog>
3008
+ <Icon name="warning" size={64} />
3009
+ <h2>Warning!</h2>
3010
+ <p>Are you absolutely sure you want to delete?</p>
3011
+ <p>This action is permanent and cannot be undone.</p>
3012
+ <p>All associated data will be lost forever.</p>
3013
+ <div>
3014
+ <button>Cancel</button>
3015
+ <button>Delete</button>
3016
+ <button>Learn More</button>
3017
+ </div>
3018
+ </dialog>
3019
+ );
3020
+ }
3021
+ ```
3022
+
3023
+ **Correct (essential information only):**
3024
+
3025
+ ```tsx
3026
+ function DeleteDialog() {
3027
+ return (
3028
+ <dialog>
3029
+ <h2>Delete this item?</h2>
3030
+ <p>This can't be undone.</p>
3031
+ <div>
3032
+ <button>Cancel</button>
3033
+ <button>Delete</button>
3034
+ </div>
3035
+ </dialog>
3036
+ );
3037
+ }
3038
+ ```
3039
+
3040
+ ### 9.23 Visually Connect Related Elements
3041
+
3042
+ Elements that are visually connected (by lines, color, or frames) are perceived as more related (Law of Uniform Connectedness).
3043
+
3044
+ **Incorrect (steps with no visual connection):**
3045
+
3046
+ ```tsx
3047
+ function Steps({ current }) {
3048
+ return (
3049
+ <div>
3050
+ <span>Step 1</span>
3051
+ <span>Step 2</span>
3052
+ <span>Step 3</span>
3053
+ </div>
3054
+ );
3055
+ }
3056
+ ```
3057
+
3058
+ **Correct (connected with a visual line):**
3059
+
3060
+ ```tsx
3061
+ function Steps({ current }) {
3062
+ return (
3063
+ <div className={styles.steps}>
3064
+ {steps.map((step, i) => (
3065
+ <div key={step.id} className={styles.step} data-active={i <= current}>
3066
+ <div className={styles.dot} />
3067
+ {i < steps.length - 1 && <div className={styles.connector} />}
3068
+ <span>{step.label}</span>
3069
+ </div>
3070
+ ))}
3071
+ </div>
3072
+ );
3073
+ }
3074
+ ```
3075
+
3076
+ Reference: [Laws of UX](https://lawsofux.com/) by Jon Yablonski
3077
+
3078
+ ---
3079
+
3080
+ ## 10. Predictive Prefetching
3081
+
3082
+ **Impact:** MEDIUM — Loading content before the user clicks by analyzing cursor trajectory, reducing perceived latency by 100-200ms.
3083
+
3084
+ ### 10.1 Trajectory Prediction Over Hover Prefetching
3085
+
3086
+ Hover prefetching starts too late. Trajectory prediction fires while the cursor is still in motion, reclaiming 100-200ms.
3087
+
3088
+ **Incorrect (waits for hover):**
3089
+
3090
+ ```tsx
3091
+ <Link
3092
+ href="/about"
3093
+ onMouseEnter={() => router.prefetch("/about")}
3094
+ >
3095
+ About
3096
+ </Link>
3097
+ ```
3098
+
3099
+ **Correct (trajectory-based):**
3100
+
3101
+ ```tsx
3102
+ const { elementRef } = useForesight({
3103
+ callback: () => router.prefetch("/about"),
3104
+ hitSlop: 20,
3105
+ name: "about-link",
3106
+ });
3107
+
3108
+ <Link ref={elementRef} href="/about">About</Link>
3109
+ ```
3110
+
3111
+ ### 10.2 Prefetch by Intent, Not Viewport
3112
+
3113
+ Don't prefetch everything visible in the viewport. Prefetch based on user intent to avoid wasted bandwidth.
3114
+
3115
+ **Incorrect (prefetch all visible links):**
3116
+
3117
+ ```tsx
3118
+ <Link href="/page" prefetch={true}>Page</Link>
3119
+ ```
3120
+
3121
+ **Correct (intent-based prefetching):**
3122
+
3123
+ ```tsx
3124
+ <Link href="/page" prefetch={false}>Page</Link>
3125
+ ```
3126
+
3127
+ ### 10.3 Use hitSlop to Trigger Predictions Earlier
3128
+
3129
+ Expand the invisible prediction area around elements with hitSlop to start loading sooner.
3130
+
3131
+ **Incorrect (tight prediction area):**
3132
+
3133
+ ```tsx
3134
+ const { elementRef } = useForesight({
3135
+ callback: () => prefetch(),
3136
+ hitSlop: 0,
3137
+ });
3138
+ ```
3139
+
3140
+ **Correct (expanded prediction area):**
3141
+
3142
+ ```tsx
3143
+ const { elementRef } = useForesight({
3144
+ callback: () => prefetch(),
3145
+ hitSlop: 20,
3146
+ });
3147
+ ```
3148
+
3149
+ ### 10.4 Fall Back Gracefully on Touch Devices
3150
+
3151
+ Touch devices have no cursor. Fall back to viewport or touch-start strategies automatically.
3152
+
3153
+ **Incorrect (assumes cursor exists):**
3154
+
3155
+ ```tsx
3156
+ function PrefetchLink({ href, children }) {
3157
+ return (
3158
+ <Link
3159
+ href={href}
3160
+ onMouseMove={() => prefetch(href)}
3161
+ >
3162
+ {children}
3163
+ </Link>
3164
+ );
3165
+ }
3166
+ ```
3167
+
3168
+ **Correct (device-aware strategy):**
3169
+
3170
+ ```tsx
3171
+ const { elementRef } = useForesight({
3172
+ callback: () => router.prefetch(href),
3173
+ hitSlop: 20,
3174
+ });
3175
+ ```
3176
+
3177
+ ### 10.5 Prefetch on Keyboard Navigation
3178
+
3179
+ Monitor focus changes and prefetch when the user is a few tab stops away from a registered element.
3180
+
3181
+ **Correct (tab-aware prefetching):**
3182
+
3183
+ ```tsx
3184
+ const { elementRef } = useForesight({
3185
+ callback: () => router.prefetch("/settings"),
3186
+ name: "settings-link",
3187
+ });
3188
+ ```
3189
+
3190
+ ### 10.6 Use Predictive Prefetching Selectively
3191
+
3192
+ Predictive prefetching doesn't belong in every project. Use it where navigation latency is noticeable.
3193
+
3194
+ **Good use cases:** data-heavy dashboards, multi-page apps with slow API responses, e-commerce product pages.
3195
+
3196
+ **Bad use cases:** static sites with instant navigation, single-page apps with all data preloaded.
3197
+
3198
+ Reference: [ForesightJS](https://foresightjs.com), [Next.js Prefetching Docs](https://nextjs.org/docs/app/guides/prefetching)
3199
+
3200
+ ---
3201
+
3202
+ ## 11. Typography
3203
+
3204
+ **Impact:** MEDIUM — CSS font and text properties most developers overlook. The difference between typographically considered and not.
3205
+
3206
+ ### 11.1 Tabular Numbers for Data Display
3207
+
3208
+ Use tabular-nums for any numeric data that should align in columns.
3209
+
3210
+ **Incorrect (proportional numbers misalign):**
3211
+
3212
+ ```css
3213
+ .price { font-variant-numeric: proportional-nums; }
3214
+ ```
3215
+
3216
+ **Correct (tabular numbers align):**
3217
+
3218
+ ```css
3219
+ .price { font-variant-numeric: tabular-nums; }
3220
+ ```
3221
+
3222
+ ### 11.2 Oldstyle Numbers for Body Text
3223
+
3224
+ Use oldstyle-nums in body text so numbers blend with lowercase letters. Use lining-nums in tables and headings.
3225
+
3226
+ **Correct (prose):**
3227
+
3228
+ ```css
3229
+ .body-text { font-variant-numeric: oldstyle-nums; }
3230
+ ```
3231
+
3232
+ **Correct (data):**
3233
+
3234
+ ```css
3235
+ .data-table { font-variant-numeric: lining-nums tabular-nums; }
3236
+ ```
3237
+
3238
+ ### 11.3 Slashed Zero for Disambiguation
3239
+
3240
+ Enable slashed zero in code-adjacent UIs so users never confuse 0 with O.
3241
+
3242
+ **Correct:**
3243
+
3244
+ ```css
3245
+ .code { font-variant-numeric: slashed-zero; }
3246
+ ```
3247
+
3248
+ ### 11.4 Enable Contextual Alternates
3249
+
3250
+ Keep contextual alternates (calt) enabled. They adjust punctuation and glyph shapes based on surrounding characters.
3251
+
3252
+ **Correct (usually on by default — don't disable):**
3253
+
3254
+ ```css
3255
+ body { font-feature-settings: "calt" 1; }
3256
+ ```
3257
+
3258
+ ### 11.5 Use Disambiguation Stylistic Set for UI
3259
+
3260
+ Enable ss02 (or your font's disambiguation set) in code-facing UIs to distinguish I, l, 1 and 0, O.
3261
+
3262
+ **Correct:**
3263
+
3264
+ ```css
3265
+ .code-ui { font-feature-settings: "ss02"; }
3266
+ ```
3267
+
3268
+ ### 11.6 Keep Optical Sizing Auto
3269
+
3270
+ Leave font-optical-sizing at auto. The font adjusts glyph shapes for the current size — thicker strokes at small sizes, finer details at large sizes.
3271
+
3272
+ **Incorrect (forced off):**
3273
+
3274
+ ```css
3275
+ body { font-optical-sizing: none; }
3276
+ ```
3277
+
3278
+ **Correct (automatic adjustment):**
3279
+
3280
+ ```css
3281
+ body { font-optical-sizing: auto; }
3282
+ ```
3283
+
3284
+ ### 11.7 Use Antialiased Font Smoothing
3285
+
3286
+ Set -webkit-font-smoothing: antialiased on retina displays. Default subpixel rendering looks thicker and fuzzier.
3287
+
3288
+ **Correct:**
3289
+
3290
+ ```css
3291
+ body {
3292
+ -webkit-font-smoothing: antialiased;
3293
+ -moz-osx-font-smoothing: grayscale;
3294
+ }
3295
+ ```
3296
+
3297
+ ### 11.8 Balance Headings with text-wrap
3298
+
3299
+ Use text-wrap: balance on headings to make lines roughly equal length instead of one long line and a short orphan.
3300
+
3301
+ **Incorrect (unbalanced heading):**
3302
+
3303
+ ```css
3304
+ h1 { /* default text-wrap */ }
3305
+ ```
3306
+
3307
+ **Correct (balanced):**
3308
+
3309
+ ```css
3310
+ h1 { text-wrap: balance; }
3311
+ ```
3312
+
3313
+ ### 11.9 Offset Underlines from Descenders
3314
+
3315
+ Use text-underline-offset to push underlines below descenders so they look intentional.
3316
+
3317
+ **Incorrect (underline collides with descenders):**
3318
+
3319
+ ```css
3320
+ a { text-decoration: underline; }
3321
+ ```
3322
+
3323
+ **Correct (offset underline):**
3324
+
3325
+ ```css
3326
+ a {
3327
+ text-decoration: underline;
3328
+ text-underline-offset: 3px;
3329
+ text-decoration-skip-ink: auto;
3330
+ }
3331
+ ```
3332
+
3333
+ ### 11.10 Disable Font Synthesis for Missing Styles
3334
+
3335
+ Set font-synthesis: none to prevent the browser from faking bold or italic. Browser-generated faux styles look terrible.
3336
+
3337
+ **Correct:**
3338
+
3339
+ ```css
3340
+ .icon-font,
3341
+ .display-font {
3342
+ font-synthesis: none;
3343
+ }
3344
+ ```
3345
+
3346
+ **Typography quick reference:**
3347
+
3348
+ | Property | Use Case | Value |
3349
+ |----------|----------|-------|
3350
+ | `font-variant-numeric: tabular-nums` | Data tables, pricing | Fixed-width digits |
3351
+ | `font-variant-numeric: oldstyle-nums` | Body text | Blends with lowercase |
3352
+ | `font-variant-numeric: slashed-zero` | Code UIs | Distinguishes 0 from O |
3353
+ | `font-feature-settings: "ss02"` | Code UIs | Disambiguates I/l/1 |
3354
+ | `font-optical-sizing: auto` | Everywhere | Size-adaptive glyphs |
3355
+ | `-webkit-font-smoothing: antialiased` | Retina displays | Thinner, cleaner text |
3356
+ | `text-wrap: balance` | Headings | Even line lengths |
3357
+ | `text-underline-offset: 3px` | Links | Clear descender space |
3358
+ | `font-synthesis: none` | Display/icon fonts | Prevents faux styles |
3359
+
3360
+ ### 11.11 Use font-display swap
3361
+
3362
+ Set font-display: swap so text renders immediately with a fallback while the custom font loads.
3363
+
3364
+ **Correct:**
3365
+
3366
+ ```css
3367
+ @font-face {
3368
+ font-family: "Inter";
3369
+ src: url("/fonts/inter.woff2") format("woff2");
3370
+ font-display: swap;
3371
+ }
3372
+ ```
3373
+
3374
+ ### 11.12 Continuous Weight Values with Variable Fonts
3375
+
3376
+ Variable fonts accept any integer from 100-900, not just standard stops.
3377
+
3378
+ **Correct (precise weight):**
3379
+
3380
+ ```css
3381
+ .medium { font-weight: 450; }
3382
+ .semibold { font-weight: 550; }
3383
+ ```
3384
+
3385
+ ### 11.13 text-wrap pretty for Body Text
3386
+
3387
+ Use text-wrap: pretty for body text to reduce orphans. Use balance for headings.
3388
+
3389
+ **Correct:**
3390
+
3391
+ ```css
3392
+ p { text-wrap: pretty; }
3393
+ h1, h2, h3 { text-wrap: balance; }
3394
+ ```
3395
+
3396
+ ### 11.14 Pair Justified Text with Hyphens
3397
+
3398
+ Justified text without hyphens creates rivers of whitespace.
3399
+
3400
+ **Incorrect (rivers):**
3401
+
3402
+ ```css
3403
+ .article { text-align: justify; }
3404
+ ```
3405
+
3406
+ **Correct (hyphenation prevents rivers):**
3407
+
3408
+ ```css
3409
+ .article {
3410
+ text-align: justify;
3411
+ hyphens: auto;
3412
+ }
3413
+ ```
3414
+
3415
+ ### 11.15 Add Letter Spacing to Uppercase Text
3416
+
3417
+ Uppercase and small-caps text needs positive letter-spacing to feel open and readable.
3418
+
3419
+ **Incorrect (tight uppercase):**
3420
+
3421
+ ```css
3422
+ .label {
3423
+ text-transform: uppercase;
3424
+ font-size: 12px;
3425
+ }
3426
+ ```
3427
+
3428
+ **Correct (opened up):**
3429
+
3430
+ ```css
3431
+ .label {
3432
+ text-transform: uppercase;
3433
+ font-size: 12px;
3434
+ letter-spacing: 0.05em;
3435
+ }
3436
+ ```
3437
+
3438
+ ### 11.16 Use Typographic Fractions
3439
+
3440
+ Enable diagonal-fractions to convert 1/2, 1/3 into proper typographic fractions.
3441
+
3442
+ **Correct:**
3443
+
3444
+ ```css
3445
+ .recipe { font-variant-numeric: diagonal-fractions; }
3446
+ ```
3447
+
3448
+ Reference: [Inter Typeface](https://rsms.me/inter/), [MDN font-feature-settings](https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings), [MDN font-variant-numeric](https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-numeric)
3449
+
3450
+ ---
3451
+
3452
+ ## 12. Visual Design
3453
+
3454
+ **Impact:** HIGH — CSS design fundamentals that compound into visual polish. Small details that separate considered interfaces from default ones.
3455
+
3456
+ ### 12.1 Concentric Border Radius for Nested Elements
3457
+
3458
+ When nesting rounded elements, inner radius must equal outer radius minus the gap. Same radius on both creates uneven curves.
3459
+
3460
+ **Incorrect (same radius on both):**
3461
+
3462
+ ```css
3463
+ .outer {
3464
+ border-radius: 16px;
3465
+ padding: 8px;
3466
+ }
3467
+
3468
+ .inner {
3469
+ border-radius: 16px;
3470
+ }
3471
+ ```
3472
+
3473
+ **Correct (concentric radius):**
3474
+
3475
+ ```css
3476
+ .outer {
3477
+ --padding: 8px;
3478
+ --inner-radius: 8px;
3479
+
3480
+ border-radius: calc(var(--inner-radius) + var(--padding));
3481
+ padding: var(--padding);
3482
+ }
3483
+
3484
+ .inner {
3485
+ border-radius: var(--inner-radius);
3486
+ }
3487
+ ```
3488
+
3489
+ ### 12.2 Layer Multiple Shadows for Realistic Depth
3490
+
3491
+ A single box-shadow looks flat. Layer multiple shadows with increasing blur and decreasing opacity to mimic real light.
3492
+
3493
+ **Incorrect (single flat shadow):**
3494
+
3495
+ ```css
3496
+ .card {
3497
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
3498
+ }
3499
+ ```
3500
+
3501
+ **Correct (layered shadows):**
3502
+
3503
+ ```css
3504
+ .card {
3505
+ box-shadow:
3506
+ 0 1px 2px rgba(0, 0, 0, 0.06),
3507
+ 0 4px 8px rgba(0, 0, 0, 0.04),
3508
+ 0 12px 24px rgba(0, 0, 0, 0.03);
3509
+ }
3510
+ ```
3511
+
3512
+ ### 12.3 Consistent Shadow Direction Across UI
3513
+
3514
+ All shadows must share the same offset direction to imply a single light source. Mixed directions feel broken.
3515
+
3516
+ **Incorrect (conflicting light sources):**
3517
+
3518
+ ```css
3519
+ .card { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); }
3520
+ .modal { box-shadow: 4px 0 8px rgba(0, 0, 0, 0.1); }
3521
+ .tooltip { box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.1); }
3522
+ ```
3523
+
3524
+ **Correct (consistent top-down light):**
3525
+
3526
+ ```css
3527
+ .card { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
3528
+ .modal { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); }
3529
+ .tooltip { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); }
3530
+ ```
3531
+
3532
+ ### 12.4 Use Neutral Colors for Shadows
3533
+
3534
+ Pure black shadows look harsh. Use deep neutrals or semi-transparent dark colors.
3535
+
3536
+ **Incorrect (pure black):**
3537
+
3538
+ ```css
3539
+ .card {
3540
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
3541
+ }
3542
+ ```
3543
+
3544
+ **Correct (neutral shadow):**
3545
+
3546
+ ```css
3547
+ .card {
3548
+ box-shadow: 0 4px 12px rgba(17, 24, 39, 0.08);
3549
+ }
3550
+ ```
3551
+
3552
+ ### 12.5 Shadow Size Indicates Elevation
3553
+
3554
+ Larger blur and offset means higher elevation. Use a consistent shadow scale.
3555
+
3556
+ **Correct (elevation scale):**
3557
+
3558
+ ```css
3559
+ :root {
3560
+ --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.05);
3561
+ --shadow-2: 0 2px 8px rgba(0, 0, 0, 0.08);
3562
+ --shadow-3: 0 8px 24px rgba(0, 0, 0, 0.12);
3563
+ }
3564
+
3565
+ .card { box-shadow: var(--shadow-1); }
3566
+ .dropdown { box-shadow: var(--shadow-2); }
3567
+ .modal { box-shadow: var(--shadow-3); }
3568
+ ```
3569
+
3570
+ ### 12.6 Animate Shadows via Pseudo-Element Opacity
3571
+
3572
+ Transitioning box-shadow directly forces expensive repaints. Animate opacity on a pseudo-element instead.
3573
+
3574
+ **Incorrect (animating box-shadow):**
3575
+
3576
+ ```css
3577
+ .card {
3578
+ box-shadow: var(--shadow-1);
3579
+ transition: box-shadow 0.2s ease;
3580
+ }
3581
+ .card:hover {
3582
+ box-shadow: var(--shadow-3);
3583
+ }
3584
+ ```
3585
+
3586
+ **Correct (pseudo-element opacity):**
3587
+
3588
+ ```css
3589
+ .card {
3590
+ position: relative;
3591
+ box-shadow: var(--shadow-1);
3592
+ }
3593
+ .card::after {
3594
+ content: "";
3595
+ position: absolute;
3596
+ inset: 0;
3597
+ border-radius: inherit;
3598
+ box-shadow: var(--shadow-3);
3599
+ opacity: 0;
3600
+ transition: opacity 0.2s ease;
3601
+ pointer-events: none;
3602
+ z-index: -1;
3603
+ }
3604
+ .card:hover::after {
3605
+ opacity: 1;
3606
+ }
3607
+ ```
3608
+
3609
+ ### 12.7 Use a Consistent Spacing Scale
3610
+
3611
+ Don't use arbitrary pixel values. Define a scale and use it throughout.
3612
+
3613
+ **Incorrect (arbitrary values):**
3614
+
3615
+ ```css
3616
+ .header { padding: 17px; }
3617
+ .card { margin-bottom: 13px; }
3618
+ .section { gap: 22px; }
3619
+ ```
3620
+
3621
+ **Correct (consistent scale):**
3622
+
3623
+ ```css
3624
+ :root {
3625
+ --space-1: 4px;
3626
+ --space-2: 8px;
3627
+ --space-3: 12px;
3628
+ --space-4: 16px;
3629
+ --space-5: 24px;
3630
+ --space-6: 32px;
3631
+ --space-7: 48px;
3632
+ }
3633
+
3634
+ .header { padding: var(--space-4); }
3635
+ .card { margin-bottom: var(--space-3); }
3636
+ .section { gap: var(--space-5); }
3637
+ ```
3638
+
3639
+ ### 12.8 Use Semi-Transparent Borders
3640
+
3641
+ Semi-transparent borders adapt to any background color and create subtle, non-jarring separation.
3642
+
3643
+ **Incorrect (hardcoded border color):**
3644
+
3645
+ ```css
3646
+ .card {
3647
+ border: 1px solid #e5e5e5;
3648
+ }
3649
+ ```
3650
+
3651
+ **Correct (alpha border):**
3652
+
3653
+ ```css
3654
+ .card {
3655
+ border: 1px solid var(--gray-a4);
3656
+ }
3657
+ ```
3658
+
3659
+ ### 12.9 Full Shadow Anatomy on Buttons
3660
+
3661
+ A polished button uses six layered techniques, not just a single box-shadow:
3662
+
3663
+ 1. **Outer cut shadow** — 0.5px dark box-shadow to "cut" the button into the surface
3664
+ 2. **Inner ambient highlight** — 1px inset box-shadow on all sides for environmental light reflections
3665
+ 3. **Inner top highlight** — 1px inset top highlight for the primary light source from above
3666
+ 4. **Layered depth shadows** — At least 3 external shadows for natural lighting
3667
+ 5. **Text drop-shadow** — Drop-shadow on text/icons for better contrast against the button background
3668
+ 6. **Subtle gradient background** — If you can tell there's a gradient, it's too much
3669
+
3670
+ **Incorrect (flat button):**
3671
+
3672
+ ```css
3673
+ .button {
3674
+ background: var(--gray-12);
3675
+ color: var(--gray-1);
3676
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
3677
+ }
3678
+ ```
3679
+
3680
+ **Correct (full shadow anatomy):**
3681
+
3682
+ ```css
3683
+ .button {
3684
+ background: linear-gradient(
3685
+ to bottom,
3686
+ color-mix(in srgb, var(--gray-12) 100%, white 4%),
3687
+ var(--gray-12)
3688
+ );
3689
+ color: var(--gray-1);
3690
+ box-shadow:
3691
+ 0 0 0 0.5px rgba(0, 0, 0, 0.3),
3692
+ inset 0 0 0 1px rgba(255, 255, 255, 0.04),
3693
+ inset 0 1px 0 rgba(255, 255, 255, 0.07),
3694
+ 0 1px 2px rgba(0, 0, 0, 0.1),
3695
+ 0 2px 4px rgba(0, 0, 0, 0.06),
3696
+ 0 4px 8px rgba(0, 0, 0, 0.03);
3697
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
3698
+ }
3699
+ ```
3700
+
3701
+ Reference: [Designing Beautiful Shadows in CSS](https://www.joshwcomeau.com/css/designing-shadows/), [Concentric Border Radius](https://jakub.kr/work/concentric-border-radius), [@PixelJanitor](https://threadreaderapp.com/thread/1623358514440859649)
3702
+
3703
+ ---
3704
+
3705
+ ## Output Format
3706
+
3707
+ When reviewing files, output findings as:
3708
+
3709
+ ```
3710
+ file:line - [rule-id] description of issue
3711
+
3712
+ Example:
3713
+ components/modal/index.tsx:45 - [timing-under-300ms] Exit animation 400ms exceeds 300ms limit
3714
+ components/button/styles.module.css:12 - [physics-active-state] Missing :active transform
3715
+ components/drawer/index.tsx:23 - [spring-for-gestures] Drag interaction using easing instead of spring
3716
+ ```
3717
+
3718
+ ## Summary Table
3719
+
3720
+ After findings, output a summary:
3721
+
3722
+ | Rule | Count | Severity |
3723
+ |------|-------|----------|
3724
+ | `timing-under-300ms` | 2 | HIGH |
3725
+ | `physics-active-state` | 3 | MEDIUM |
3726
+ | `exit-requires-wrapper` | 1 | HIGH |
3727
+
3728
+ ## References
3729
+
3730
+ - [The Illusion of Life: Disney Animation](https://www.amazon.com/Illusion-Life-Disney-Animation/dp/0786860707)
3731
+ - [Apple WWDC23: Animate with Springs](https://developer.apple.com/videos/play/wwdc2023/10158)
3732
+ - [Motion Documentation](https://motion.dev)
3733
+ - [The Beauty of Bezier Curves - Freya Holmer](https://www.youtube.com/watch?v=aVwxzDHniEw)
3734
+ - [MDN Pseudo-elements Reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-elements)
3735
+ - [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)
3736
+ - [Web Audio API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
3737
+ - [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)
3738
+ - [SVG Line Element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
3739
+ - [ResizeObserver - MDN](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
3740
+ - [Laws of UX](https://lawsofux.com/) by Jon Yablonski
3741
+ - [ForesightJS](https://foresightjs.com)
3742
+ - [Next.js Prefetching Docs](https://nextjs.org/docs/app/guides/prefetching)
3743
+ - [Inter Typeface](https://rsms.me/inter/)
3744
+ - [MDN font-feature-settings](https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings)
3745
+ - [MDN font-variant-numeric](https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-numeric)
3746
+ - [Designing Beautiful Shadows in CSS - Josh W. Comeau](https://www.joshwcomeau.com/css/designing-shadows/)
3747
+ - [Concentric Border Radius](https://jakub.kr/work/concentric-border-radius)
3748
+ - [Nested Rounded Corners](https://www.ondrejkonecny.com/blog/nested-rounded-corners/)
3749
+ - [MDN text-wrap](https://developer.mozilla.org/en-US/docs/Web/CSS/text-wrap)