@its-thepoe/design-motion-principles 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ # Common Mistakes
2
+
3
+ ---
4
+
5
+ ## From Emil's Perspective (Purposeful Restraint)
6
+
7
+ - **Animating high-frequency interactions** — If users trigger this 100s of times daily, remove the animation
8
+ - **Animating keyboard-initiated actions** — Keyboard shortcuts should NEVER animate
9
+ - **Animations over 300ms** — UI animations should be under 300ms; 180ms feels more responsive than 400ms
10
+ - **Animating from scale(0)** — Start from `scale(0.9)` or higher for natural motion
11
+ - **Same tooltip behavior everywhere** — First tooltip: delayed + animated. Subsequent: instant
12
+ - **Using default CSS easing** — Built-in `ease` and `ease-in-out` lack strength; use custom curves
13
+ - **Ignoring transform-origin** — Dropdowns should expand from their trigger, not center
14
+ - **Expecting delight in productivity tools** — Users of high-frequency tools prioritize speed over delight
15
+ - **Using keyframes for interruptible animations** — Keyframes can't retarget mid-flight; use CSS transitions with state
16
+ - **CSS variables for frequent updates** — Causes expensive style recalculation; update styles directly on element
17
+ - **Distance thresholds for dismissal** — Use velocity (distance/time) instead; fast short gestures should work
18
+ - **Abrupt boundary stops** — Use damping; things slow down before stopping in real life
19
+
20
+ ---
21
+
22
+ ## From Jakub's Perspective (Production Polish)
23
+
24
+ - **Making enter and exit animations equally prominent** — Exits should be subtler
25
+ - **Using solid borders when shadows would adapt better** — Especially on varied backgrounds
26
+ - **Forgetting optical alignment** — Buttons with icons, play buttons, asymmetric shapes
27
+ - **Over-animating** — If users notice the animation itself, it's too much
28
+ - **Using the same animation everywhere** — Context should drive timing and easing choices
29
+ - **Ignoring hover state transitions** — Even small transitions (150-200ms) feel more polished than instant changes
30
+
31
+ ---
32
+
33
+ ## From Jhey's Perspective (Creative Learning)
34
+
35
+ - **Filtering ideas based on "usefulness" too early** — Make first, judge later
36
+ - **Not documenting random creative sparks** — Keep notebooks everywhere, including by your bed
37
+ - **Thinking CSS art is useless** — It teaches real skills (clip-path, layering, complex shapes)
38
+ - **Focusing on "How do I learn X?" instead of "How do I make Y?"** — Let ideas drive learning
39
+ - **Following tutorials without experimenting** — Tutorials teach techniques; experimentation teaches problem-solving
40
+ - **Giving up when something doesn't work** — The struggle is where learning happens
41
+
42
+ ---
43
+
44
+ ## General Motion Design Mistakes
45
+
46
+ - **Animating layout-triggering properties** (width, height, top, left) — Use transform instead
47
+ - **No animation at all** — Instant state changes feel broken to modern users
48
+ - **Same duration for all animations** — Smaller elements should animate faster
49
+ - **Forgetting `prefers-reduced-motion`** — Not optional
50
+
51
+ *Note: Duration is designer-dependent. Emil prefers under 300ms for productivity tools. Jakub and Jhey may use longer durations when polish or effect warrants it.*
52
+
53
+ ---
54
+
55
+ ## Red Flags in Code Review
56
+
57
+ Watch for these patterns:
58
+
59
+ ```jsx
60
+ // BAD: Animating layout properties
61
+ animate={{ width: 200, height: 100 }}
62
+
63
+ // GOOD: Use transform
64
+ animate={{ scale: 1.2 }}
65
+ ```
66
+
67
+ ```jsx
68
+ // BAD: Same animation for enter and exit
69
+ initial={{ opacity: 0, y: 20 }}
70
+ exit={{ opacity: 0, y: 20 }}
71
+
72
+ // GOOD: Subtler exit
73
+ initial={{ opacity: 0, y: 20 }}
74
+ exit={{ opacity: 0, y: -8 }}
75
+ ```
76
+
77
+ ```css
78
+ /* BAD: No reduced motion support */
79
+ .animated { animation: bounce 1s infinite; }
80
+
81
+ /* GOOD: Respects user preference */
82
+ @media (prefers-reduced-motion: no-preference) {
83
+ .animated { animation: bounce 1s infinite; }
84
+ }
85
+ ```
86
+
87
+ ```css
88
+ /* BAD: will-change everywhere */
89
+ * { will-change: transform; }
90
+
91
+ /* GOOD: Targeted will-change */
92
+ .animated-button { will-change: transform, opacity; }
93
+ ```
94
+
95
+ ```jsx
96
+ // BAD: Animating from scale(0) (Emil)
97
+ initial={{ scale: 0 }}
98
+ animate={{ scale: 1 }}
99
+
100
+ // GOOD: Start from higher scale
101
+ initial={{ scale: 0.9, opacity: 0 }}
102
+ animate={{ scale: 1, opacity: 1 }}
103
+ ```
104
+
105
+ ```jsx
106
+ // Per Emil: Too slow for productivity UI
107
+ transition={{ duration: 0.4 }}
108
+
109
+ // Per Emil: Fast, snappy (but Jakub/Jhey might use 0.4 for polish)
110
+ transition={{ duration: 0.18 }}
111
+ ```
112
+
113
+ ```css
114
+ /* BAD: Dropdown expanding from center (Emil) */
115
+ .dropdown {
116
+ transform-origin: center;
117
+ }
118
+
119
+ /* GOOD: Origin-aware animation */
120
+ .dropdown {
121
+ transform-origin: top center;
122
+ }
123
+ ```
124
+
125
+ ```css
126
+ /* BAD: Keyframes can't be interrupted (Emil) */
127
+ @keyframes slideIn {
128
+ from { transform: translateY(100%); }
129
+ to { transform: translateY(0); }
130
+ }
131
+ .toast { animation: slideIn 400ms ease; }
132
+
133
+ /* GOOD: Transitions can retarget mid-flight */
134
+ .toast {
135
+ transform: translateY(100%);
136
+ transition: transform 400ms ease;
137
+ }
138
+ .toast.mounted {
139
+ transform: translateY(0);
140
+ }
141
+ ```
142
+
143
+ ```javascript
144
+ // BAD: CSS variables cause cascade recalc (Emil)
145
+ element.style.setProperty('--drag-y', `${y}px`);
146
+
147
+ // GOOD: Direct style update
148
+ element.style.transform = `translateY(${y}px)`;
149
+ ```
150
+
151
+ ```javascript
152
+ // BAD: Distance threshold for dismissal (Emil)
153
+ if (dragDistance > 100) dismiss();
154
+
155
+ // GOOD: Velocity-based (fast short gestures work)
156
+ const velocity = dragDistance / elapsedTime;
157
+ if (velocity > 0.11) dismiss();
158
+ ```
@@ -0,0 +1,355 @@
1
+ # Emil Kowalski's Animation Principles
2
+
3
+ Emil Kowalski is a Design Engineer at Linear (previously Vercel). Creator of Sonner, Vaul, and the "Animations on the Web" course. His approach emphasizes **restraint, speed, and purposeful motion**.
4
+
5
+ ---
6
+
7
+ ## Core Philosophy: Restraint & Purpose
8
+
9
+ Emil's defining contribution to motion design thinking is knowing **when NOT to animate**.
10
+
11
+ > "The goal is not to animate for animation's sake, it's to build great user interfaces."
12
+
13
+ ### The Frequency Rule
14
+
15
+ **Animation appropriateness depends on interaction frequency:**
16
+
17
+ | Frequency | Recommendation |
18
+ |-----------|----------------|
19
+ | Rare (monthly) | Delightful, morphing animations welcome |
20
+ | Occasional (daily) | Subtle, fast animations |
21
+ | Frequent (100s/day) | No animation or instant transitions |
22
+ | Keyboard-initiated | Never animate |
23
+
24
+ **The Raycast example**: A tool used constantly throughout the day benefits from zero animation. Users with clear goals "don't expect to be delighted" and prioritize frictionless workflow.
25
+
26
+ ### Speed is Non-Negotiable
27
+
28
+ > "UI animations should generally stay under 300ms."
29
+
30
+ A 180ms animation feels more responsive than 400ms. Speed creates perceived performance. When in doubt, go faster.
31
+
32
+ ---
33
+
34
+ ## The 7 Practical Animation Tips
35
+
36
+ ### 1. Scale Your Buttons
37
+ Add subtle scale-down on press for immediate feedback:
38
+ ```css
39
+ button:active {
40
+ transform: scale(0.97);
41
+ }
42
+ ```
43
+
44
+ ### 2. Don't Animate from scale(0)
45
+ Animating from `scale(0)` creates unnatural motion. Start from `scale(0.9)` or higher for elegant, gentle movement:
46
+ ```jsx
47
+ // BAD
48
+ initial={{ scale: 0 }}
49
+
50
+ // GOOD
51
+ initial={{ scale: 0.9, opacity: 0 }}
52
+ ```
53
+
54
+ ### 3. Tooltip Delay Patterns
55
+ First tooltip: delay + animation. Subsequent tooltips in same group: instant (no delay, no animation):
56
+ ```css
57
+ [data-instant] {
58
+ transition-duration: 0ms;
59
+ }
60
+ ```
61
+
62
+ ### 4. Custom Easing is Essential
63
+
64
+ > "Easing is the most important part of any animation. It can make a bad animation feel great."
65
+
66
+ **Curve choice:** Don’t rely on generic CSS easings for motion-critical UI. Prefer explicit **`cubic-bezier(...)`** values. The only built-ins to use by default are **`ease`** (see hover note below) and **`linear`** where appropriate.
67
+
68
+ #### `ease-in` (starts slow, speeds up)
69
+
70
+ Avoid for anything that should feel immediately responsive.
71
+
72
+ - `ease-in-quad`: `cubic-bezier(.55, .085, .68, .53)`
73
+ - `ease-in-cubic`: `cubic-bezier(.550, .055, .675, .19)`
74
+ - `ease-in-quart`: `cubic-bezier(.895, .03, .685, .22)`
75
+ - `ease-in-quint`: `cubic-bezier(.755, .05, .855, .06)`
76
+ - `ease-in-expo`: `cubic-bezier(.95, .05, .795, .035)`
77
+ - `ease-in-circ`: `cubic-bezier(.6, .04, .98, .335)`
78
+
79
+ #### `ease-out` (starts fast, slows down)
80
+
81
+ Use for elements **entering** the screen and **user-initiated** interactions.
82
+
83
+ - `ease-out-quad`: `cubic-bezier(.25, .46, .45, .94)`
84
+ - `ease-out-cubic`: `cubic-bezier(.215, .61, .355, 1)`
85
+ - `ease-out-quart`: `cubic-bezier(.165, .84, .44, 1)`
86
+ - `ease-out-quint`: `cubic-bezier(.23, 1, .32, 1)`
87
+ - `ease-out-expo`: `cubic-bezier(.19, 1, .22, 1)`
88
+ - `ease-out-circ`: `cubic-bezier(.075, .82, .165, 1)`
89
+
90
+ #### `ease-in-out` (smooth acceleration and deceleration)
91
+
92
+ Use for elements **moving within** the screen (repositioning, not a pure “enter from off-screen” moment).
93
+
94
+ - `ease-in-out-quad`: `cubic-bezier(.455, .03, .515, .955)`
95
+ - `ease-in-out-cubic`: `cubic-bezier(.645, .045, .355, 1)`
96
+ - `ease-in-out-quart`: `cubic-bezier(.77, 0, .175, 1)`
97
+ - `ease-in-out-quint`: `cubic-bezier(.86, 0, .07, 1)`
98
+ - `ease-in-out-expo`: `cubic-bezier(1, 0, 0, 1)`
99
+ - `ease-in-out-circ`: `cubic-bezier(.785, .135, .15, .86)`
100
+
101
+ **Defaults:**
102
+
103
+ - **Entering / user-driven:** default toward **`ease-out`** family.
104
+ - **Moving within layout:** default toward **`ease-in-out`** family.
105
+ - **Springs:** use for **interactive** elements (drag, press, follow-cursor) when the stack supports it — keep professional UIs low-bounce unless the interaction is drag-driven.
106
+
107
+ **Timing:** keep general UI motion **fast** — typically **~0.2–0.3s**; avoid going past **~1s** unless the motion is **illustrative** (hero, onboarding moment), not utility UI.
108
+
109
+ **Simple hovers** (`color`, `background-color`, `opacity`): built-in **`ease`**, **200ms**, and gate hovers on fine devices:
110
+
111
+ ```css
112
+ @media (hover: hover) and (pointer: fine) {
113
+ /* hover transitions */
114
+ }
115
+ ```
116
+
117
+ **Extra references (optional):** [easing.dev](https://easing.dev), [easings.co](https://easings.co)
118
+
119
+ ### 5. Origin-Aware Animations
120
+ Animations should originate from their logical source:
121
+ ```css
122
+ /* Dropdown from button should expand from button, not center */
123
+ .dropdown {
124
+ transform-origin: top center; /* or use CSS variables from Radix/Base UI */
125
+ }
126
+ ```
127
+
128
+ Base UI: `--transform-origin`
129
+ Radix UI: `--radix-dropdown-menu-content-transform-origin`
130
+
131
+ ### 6. Keep Animations Fast
132
+
133
+ - **Duration:** aim **under 300ms** for utility UI; **~180ms** often feels better than **400ms**. Typical band **~0.2–0.3s** (see §4); illustrative / hero motion can run longer (cap ~1s unless truly decorative).
134
+ - Remove animations entirely for **high-frequency** interactions (see Frequency Rule above).
135
+
136
+ ### 7. Use Blur When Nothing Else Works
137
+ Add `filter: blur(2px)` to mask imperfections during state transitions:
138
+ ```css
139
+ .transitioning {
140
+ filter: blur(2px);
141
+ }
142
+ ```
143
+ Blur bridges the gap between old and new states, creating smoother perceived motion.
144
+
145
+ ---
146
+
147
+ ## Clip-Path Mastery
148
+
149
+ Emil advocates for `clip-path` as a powerful animation primitive.
150
+
151
+ ### Why clip-path?
152
+ - Hardware-accelerated
153
+ - No layout shifts
154
+ - No additional DOM elements needed
155
+ - Smoother than width/height animations
156
+
157
+ ### Image Reveal Effect
158
+ ```css
159
+ .reveal {
160
+ clip-path: inset(0 0 100% 0);
161
+ animation: reveal 1s forwards cubic-bezier(0.77, 0, 0.175, 1);
162
+ }
163
+
164
+ @keyframes reveal {
165
+ to { clip-path: inset(0 0 0 0); }
166
+ }
167
+ ```
168
+
169
+ ### Tab Transitions with clip-path
170
+ Duplicate tab lists with different styling. Animate overlay's clip-path to reveal only active tab—creates smooth color transitions that blend naturally with movement.
171
+
172
+ ### Scroll-Driven with clip-path
173
+ ```javascript
174
+ const clipPathY = useTransform(scrollYProgress, [0, 1], ["100%", "0%"]);
175
+ const motionClipPath = useMotionTemplate`inset(0 0 ${clipPathY} 0)`;
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Spring Physics
181
+
182
+ Emil uses spring animations extensively with three key parameters:
183
+
184
+ | Parameter | Effect |
185
+ |-----------|--------|
186
+ | **Stiffness** | How quickly the spring reaches target (higher = faster) |
187
+ | **Damping** | How quickly oscillations settle (higher = less bounce) |
188
+ | **Mass** | Weight of the object (higher = more momentum) |
189
+
190
+ ### Spring for Mouse Position
191
+ Use `useSpring` for mouse-position-driven components:
192
+ ```javascript
193
+ const springConfig = { stiffness: 300, damping: 30 };
194
+ const x = useSpring(mouseX, springConfig);
195
+ const y = useSpring(mouseY, springConfig);
196
+ ```
197
+
198
+ This interpolates value changes with spring behavior rather than instant updates—nothing in the real world changes instantly.
199
+
200
+ ---
201
+
202
+ ## Interruptibility
203
+
204
+ Great animations can be interrupted mid-play and respond naturally:
205
+
206
+ - Framer Motion supports interruption natively
207
+ - CSS transitions allow smooth interruption before completion
208
+ - Enable mid-animation state changes for responsive feel
209
+
210
+ Test by clicking rapidly—animations should blend smoothly, not queue up.
211
+
212
+ ---
213
+
214
+ ## Performance Principles
215
+
216
+ ### Hardware Acceleration
217
+ - Animate `transform` and `opacity` only (composite rendering)
218
+ - Avoid `padding`/`margin` changes (trigger layout, paint, composite)
219
+ - CSS and WAAPI animations remain smooth regardless of main thread load
220
+
221
+ ### The 60fps Target
222
+ - Minimum 60 frames per second
223
+ - Test with DevTools Performance panel
224
+ - Watch for dropped frames during scroll or interaction
225
+
226
+ ### Shared Layout Animation Gotcha
227
+ Framer Motion's shared layout animations can drop frames when browser is busy. For critical animations, CSS solutions move computation off CPU.
228
+
229
+ ---
230
+
231
+ ## When to Use Each Approach
232
+
233
+ | Context | Approach |
234
+ |---------|----------|
235
+ | Keyboard shortcuts | No animation |
236
+ | High-frequency tool | Minimal or no animation |
237
+ | Daily-use feature | Fast, subtle animation (180-250ms) |
238
+ | Onboarding/first-time | Delightful animations welcome |
239
+ | Marketing/landing page | Full creative expression |
240
+ | Banking/serious UI | Minimal, functional motion |
241
+ | Playful brand | Bouncy, elastic easing appropriate |
242
+
243
+ ---
244
+
245
+ ## Implementation Patterns from Sonner & Vaul
246
+
247
+ Emil's open-source component libraries (Sonner for toasts, Vaul for drawers) reveal his philosophy in actual code.
248
+
249
+ ### CSS Transitions Over Keyframes
250
+
251
+ Keyframes can't be interrupted mid-animation. When users rapidly trigger actions, elements "jump" rather than smoothly retargeting. Use CSS transitions with state-driven classes:
252
+
253
+ ```jsx
254
+ // After mount, set state to trigger transition
255
+ useEffect(() => {
256
+ setMounted(true);
257
+ }, []);
258
+
259
+ // CSS handles the actual animation
260
+ .toast {
261
+ transform: translateY(100%);
262
+ transition: transform 400ms ease;
263
+ }
264
+ .toast.mounted {
265
+ transform: translateY(0);
266
+ }
267
+ ```
268
+
269
+ ### Direct Style Updates for Performance
270
+
271
+ CSS variables cause style recalculation across all child elements. For frequent updates (drag operations), update styles directly on the element:
272
+
273
+ ```javascript
274
+ // BAD: CSS variable (expensive recalculation)
275
+ element.style.setProperty('--drag-y', `${y}px`);
276
+
277
+ // GOOD: Direct style (no cascade)
278
+ element.style.transform = `translateY(${y}px)`;
279
+ ```
280
+
281
+ ### Match Native Motion Curves
282
+
283
+ Vaul uses `cubic-bezier(0.32, 0.72, 0, 1)` derived from Ionic Framework to match iOS sheet animations. Duration: 500ms. Familiarity creates perceived quality.
284
+
285
+ ### Momentum-Based Dismissal
286
+
287
+ Don't require distance thresholds—use velocity (distance / time):
288
+
289
+ ```javascript
290
+ const velocity = dragDistance / elapsedTime;
291
+ if (velocity > 0.11) {
292
+ dismiss(); // Fast, short gestures work
293
+ }
294
+ ```
295
+
296
+ Threshold of `0.11` was found through iteration, not calculation.
297
+
298
+ ### Damping for Natural Motion
299
+
300
+ > "Things in real life don't suddenly stop, they slow down first."
301
+
302
+ When dragging past boundaries, reduce movement progressively rather than stopping abruptly.
303
+
304
+ ### Multi-Touch Protection
305
+
306
+ Ignore additional touches after the first until release. Prevents position jumps that feel broken.
307
+
308
+ ### Pointer Capture for Drag UX
309
+
310
+ Call `setPointerCapture()` during drags so tracking continues even when pointer leaves the element boundary.
311
+
312
+ ### Invisible Quality Details
313
+
314
+ - **Document visibility**: Pause timers when tab is inactive
315
+ - **Hover gap-filling**: `:after` pseudo-elements bridge spacing between stacked elements
316
+ - **Scroll-to-drag timeout**: 100ms delay prevents accidental dismissal from momentum
317
+
318
+ ### Sonner Defaults
319
+
320
+ | Setting | Value | Rationale |
321
+ |---------|-------|-----------|
322
+ | Duration | 4000ms | Long enough to read, short enough to not annoy |
323
+ | Animation | 400ms ease | Smooth but snappy |
324
+ | Position | bottom-right | Convention, out of primary content |
325
+ | Dismissible | true | User control by default |
326
+
327
+ ### Vaul Defaults
328
+
329
+ | Setting | Value | Rationale |
330
+ |---------|-------|-----------|
331
+ | Duration | 500ms | Match iOS sheet feel |
332
+ | Easing | cubic-bezier(0.32, 0.72, 0, 1) | iOS-native curve |
333
+ | Modal | true | Focus management, overlay |
334
+ | Direction | bottom | Convention for mobile sheets |
335
+
336
+ ---
337
+
338
+ ## The Core Philosophy
339
+
340
+ > "When a feature functions as you assume it should, you proceed without giving it a second thought, which is our goal."
341
+
342
+ Every detail—from multi-touch handling to damping to pointer capture—serves **invisible quality**. Users shouldn't notice polished interactions; they should just feel right.
343
+
344
+ ---
345
+
346
+ ## Emil vs. Jakub vs. Jhey
347
+
348
+ | Aspect | Emil | Jakub | Jhey |
349
+ |--------|------|-------|------|
350
+ | **Focus** | Restraint & speed | Subtle polish | Playful experimentation |
351
+ | **Key question** | "Should this animate?" | "Is this subtle enough?" | "What could this become?" |
352
+ | **Signature technique** | Frequency-based decisions | Blur + opacity + translateY | CSS custom properties |
353
+ | **Ideal context** | High-frequency tools | Production polish | Learning & exploration |
354
+
355
+ **Synthesis**: Use Emil's framework to decide IF you should animate. Use Jakub's techniques for HOW to animate in production. Use Jhey's approach for learning and experimentation.