@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,527 @@
1
+ # Technical Principles
2
+
3
+ ---
4
+
5
+ ## 1. Enter & Exit Animations
6
+
7
+ ### Enter Animation Recipe (Jakub)
8
+ A standard enter animation combines three properties:
9
+ - **Opacity**: 0 → 1
10
+ - **TranslateY**: ~8px → 0 (or calc(-100% - 4px) for full container slides)
11
+ - **Blur**: 4px → 0px
12
+
13
+ ```jsx
14
+ initial={{ opacity: 0, translateY: "calc(-100% - 4px)", filter: "blur(4px)" }}
15
+ animate={{ opacity: 1, translateY: 0, filter: "blur(0px)" }}
16
+ transition={{ type: "spring", duration: 0.45, bounce: 0 }}
17
+ ```
18
+
19
+ **Why blur?** It creates a "materializing" effect that feels more physical than opacity alone. The element appears to come into focus, not just fade in.
20
+
21
+ ### Exit Animation Subtlety (Jakub)
22
+ **Key Insight**: Exit animations should be subtler than enter animations.
23
+
24
+ When a component exits, it doesn't need the same amount of movement or attention as when entering. The user's focus is moving to what comes next, not what's leaving.
25
+
26
+ ```jsx
27
+ // Instead of full exit movement:
28
+ exit={{ translateY: "calc(-100% - 4px)" }}
29
+
30
+ // Use a subtle fixed value:
31
+ exit={{ translateY: "-12px", opacity: 0, filter: "blur(4px)" }}
32
+ ```
33
+
34
+ **Why this works**: Exits become softer, less jarring, and don't compete for attention with whatever is entering or remaining.
35
+
36
+ **When NOT to use subtle exits**:
37
+ - When the exit itself is meaningful (user-initiated dismissal)
38
+ - When you need to emphasize something leaving (error clearing, item deletion)
39
+ - Full-page transitions where directional continuity matters
40
+
41
+ ### Fill Mode for Persistence (Jhey)
42
+ Use `animation-fill-mode` to prevent jarring visual resets:
43
+ - `forwards`: Element retains animation styling after completion
44
+ - `backwards`: Element retains style from first keyframe before animation starts
45
+ - `both`: Retains styling in both directions
46
+
47
+ **Critical for**: Fade-in sequences with delays. Without `backwards`, elements flash at full opacity before their delayed animation starts, then pop to invisible, then fade in.
48
+
49
+ ---
50
+
51
+ ## 2. Easing & Timing
52
+
53
+ ### Duration Impacts Naturalness
54
+ > "Duration is all about timing, and timing has a big impact on the movement's naturalness." — Jhey Tompkins
55
+
56
+ ### Custom Easing is Essential (Emil)
57
+ > "Easing is the most important part of any animation. It can make a bad animation feel great."
58
+
59
+ Built-in CSS easing (`ease`, `ease-in-out`) lacks strength. Always use custom Bézier curves for professional results. Resources: easing.dev, easings.co
60
+
61
+ ### Easing Selection Guidelines (Jhey)
62
+ Each easing curve communicates something to the viewer. **Context matters more than rules.**
63
+
64
+ | Easing | Feel | Good For |
65
+ |--------|------|----------|
66
+ | `ease-out` | Fast start, gentle stop | Elements entering view (arriving) |
67
+ | `ease-in` | Gentle start, fast exit | Elements leaving view (departing) |
68
+ | `ease-in-out` | Gentle both ends | Elements changing state while visible |
69
+ | `linear` | Constant speed | Continuous loops, progress indicators |
70
+ | `spring` | Natural deceleration | Interactive elements, professional UI |
71
+
72
+ **The Context Rule**:
73
+ > "You wouldn't use 'Elastic' for a bank's website, but it might work perfectly for an energetic site for children."
74
+
75
+ Brand personality should drive easing choices. A playful brand can use bouncy, elastic easing. A professional brand should use subtle springs or ease-out.
76
+
77
+ **When NOT to use bouncy/elastic easing**:
78
+ - Professional/enterprise applications
79
+ - Frequently repeated interactions (gets tiresome)
80
+ - Error states or serious UI
81
+ - When users need to complete tasks quickly
82
+
83
+ ### Spring Animations (Jakub)
84
+ Prefer spring animations over linear/ease for more natural-feeling motion:
85
+ ```jsx
86
+ transition={{ type: "spring", duration: 0.45, bounce: 0 }}
87
+ transition={{ type: "spring", duration: 0.55, bounce: 0.1 }}
88
+ ```
89
+
90
+ **Why `bounce: 0`?** It gives smooth deceleration without overshoot—professional and refined. Reserve bounce > 0 for playful contexts.
91
+
92
+ ### The linear() Function (Jhey)
93
+ CSS `linear()` enables bounce, elastic, and spring effects in pure CSS:
94
+ ```css
95
+ :root {
96
+ --bounce-easing: linear(
97
+ 0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765,
98
+ 1, 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785,
99
+ 0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953,
100
+ 0.973, 1, 0.988, 0.984, 0.988, 1
101
+ );
102
+ }
103
+ ```
104
+
105
+ Use Jake Archibald's linear() generator for custom curves: https://linear-easing-generator.netlify.app/
106
+
107
+ ### Stagger Techniques (Jhey)
108
+ `animation-delay` only applies once (not per iteration). Approaches:
109
+
110
+ 1. **Different delays with finite iterations** — Works for one-time sequences
111
+ 2. **Pad keyframes** to create stagger within the animation:
112
+ ```css
113
+ @keyframes spin {
114
+ 0%, 50% { transform: rotate(0deg); }
115
+ 100% { transform: rotate(360deg); }
116
+ }
117
+ ```
118
+
119
+ 3. **Negative delays** for "already in progress" effects:
120
+ ```css
121
+ .element { animation-delay: calc(var(--index) * -0.2s); }
122
+ ```
123
+ This makes animations appear mid-flight from the start—useful for staggered continuous animations.
124
+
125
+ ---
126
+
127
+ ## 3. Visual Effects
128
+
129
+ ### Shadows Instead of Borders (Jakub)
130
+ In light mode, prefer subtle multi-layer box-shadows over solid borders:
131
+ ```css
132
+ .card {
133
+ box-shadow:
134
+ 0px 0px 0px 1px rgba(0, 0, 0, 0.06),
135
+ 0px 1px 2px -1px rgba(0, 0, 0, 0.06),
136
+ 0px 2px 4px 0px rgba(0, 0, 0, 0.04);
137
+ }
138
+
139
+ /* Slightly darker on hover */
140
+ .card:hover {
141
+ box-shadow:
142
+ 0px 0px 0px 1px rgba(0, 0, 0, 0.08),
143
+ 0px 1px 2px -1px rgba(0, 0, 0, 0.08),
144
+ 0px 2px 4px 0px rgba(0, 0, 0, 0.06);
145
+ }
146
+ ```
147
+
148
+ **Why shadows over borders?**
149
+ - Shadows adapt to any background (images, gradients, varied colors) because they use transparency
150
+ - Borders are solid colors that may clash with dynamic backgrounds
151
+ - Multi-layer shadows create depth; single borders feel flat
152
+ - Shadows can be transitioned smoothly with `transition: box-shadow`
153
+
154
+ **When borders are fine**:
155
+ - Dark mode (shadows less visible anyway)
156
+ - When you need hard edges intentionally
157
+ - Simple interfaces where depth isn't needed
158
+
159
+ ### Gradients & Color Spaces (Jakub)
160
+ - Use `oklch` for gradients to avoid muddy midpoints:
161
+ ```css
162
+ element { background: linear-gradient(in oklch, blue, red); }
163
+ ```
164
+
165
+ - **Color hints** control where the blend midpoint appears (different from color stops)
166
+ - Layer gradients with `background-blend-mode` for unique effects
167
+
168
+ **Why oklch?** It interpolates through perceptually uniform color space, avoiding the gray/muddy zone that sRGB hits when blending complementary colors.
169
+
170
+ ### Blur as a Signal (Jakub)
171
+ Blur (via `filter: blur()`) combined with opacity and translate creates a "materializing" effect. Use blur to signal:
172
+ - **Entering focus**: blur → sharp
173
+ - **Losing relevance**: sharp → blur
174
+ - **State transitions**: blur during, sharp after
175
+
176
+ ---
177
+
178
+ ## 4. Optical Alignment
179
+
180
+ ### Geometric vs. Optical (Jakub)
181
+ > "Sometimes it's necessary to break out of geometric alignment to make things feel visually balanced."
182
+
183
+ **Buttons with icons**: Reduce padding on the icon side so content appears centered:
184
+ ```
185
+ [ Icon Text ] ← Geometric (mathematically centered, feels off)
186
+ [ Icon Text ] ← Optical (visually centered, feels right)
187
+ ```
188
+
189
+ **Play button icons**: The triangle points right, creating visual weight on the left. Shift it slightly right to appear centered.
190
+
191
+ **Icons in general**: Many icon packs account for optical balance, but asymmetric shapes (arrows, play, chevrons) may need manual margin/padding adjustment.
192
+
193
+ **The rule**: If it looks wrong despite being mathematically correct, trust your eyes and adjust.
194
+
195
+ ---
196
+
197
+ ## 5. Icon & State Animations (Jakub)
198
+
199
+ ### Contextual Icon Transitions
200
+ When icons change contextually (copy → check, loading → done), animate:
201
+ - Opacity
202
+ - Scale
203
+ - Blur
204
+
205
+ ```jsx
206
+ <AnimatePresence mode="wait">
207
+ {isCopied ? (
208
+ <motion.div
209
+ initial={{ opacity: 0, scale: 0.8, filter: "blur(4px)" }}
210
+ animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
211
+ exit={{ opacity: 0, scale: 0.8, filter: "blur(4px)" }}
212
+ >
213
+ <CheckIcon />
214
+ </motion.div>
215
+ ) : (
216
+ <motion.div ...>
217
+ <CopyIcon />
218
+ </motion.div>
219
+ )}
220
+ </AnimatePresence>
221
+ ```
222
+
223
+ **Why animate icon swaps?** Instant swaps feel jarring and can be missed. Animated transitions:
224
+ - Draw attention to the state change
225
+ - Feel responsive and polished
226
+ - Give the user confidence their action registered
227
+
228
+ ---
229
+
230
+ ## 6. Shared Layout Animations (Jakub)
231
+
232
+ ### FLIP Technique via layoutId
233
+ Motion's `layoutId` prop enables smooth transitions between completely different components:
234
+ ```jsx
235
+ // In one location:
236
+ <motion.div layoutId="card" className="small-card" />
237
+
238
+ // In another location:
239
+ <motion.div layoutId="card" className="large-card" />
240
+ ```
241
+
242
+ Motion automatically animates between them using the FLIP technique (First, Last, Inverse, Play).
243
+
244
+ ### Best Practices
245
+ - Keep elements with `layoutId` **outside** of `AnimatePresence` to avoid conflicts
246
+ - If inside `AnimatePresence`, the initial/exit animations will trigger during layout animation (looks bad with opacity)
247
+ - Multiple elements can animate if each has a unique `layoutId`
248
+ - Works for different heights, widths, positions, and even component types (card → modal)
249
+
250
+ ---
251
+
252
+ ## 7. CSS Custom Properties & @property (Jhey)
253
+
254
+ ### Type Specification Unlocks Animation
255
+ The `@property` rule lets you declare types for CSS variables, enabling smooth interpolation:
256
+
257
+ ```css
258
+ @property --hue {
259
+ initial-value: 0;
260
+ inherits: false;
261
+ syntax: '<number>';
262
+ }
263
+
264
+ @keyframes rainbow {
265
+ to { --hue: 360; }
266
+ }
267
+ ```
268
+
269
+ **Available types**: length, number, percentage, color, angle, time, integer, transform-list
270
+
271
+ **Why this matters**: Without `@property`, CSS sees custom properties as strings. Strings can't interpolate—they just swap. With a declared type, the browser knows how to smoothly transition between values.
272
+
273
+ ### Decompose Complex Transforms
274
+ Instead of animating a monolithic transform (which can't interpolate curved paths), split into typed properties:
275
+
276
+ ```css
277
+ @property --x { syntax: '<percentage>'; initial-value: 0%; inherits: false; }
278
+ @property --y { syntax: '<percentage>'; initial-value: 0%; inherits: false; }
279
+
280
+ .ball {
281
+ transform: translateX(var(--x)) translateY(var(--y));
282
+ animation: throw 1s;
283
+ }
284
+
285
+ @keyframes throw {
286
+ 0% { --x: -500%; }
287
+ 50% { --y: -250%; }
288
+ 100% { --x: 500%; }
289
+ }
290
+ ```
291
+
292
+ This creates curved motion paths that would be impossible with standard transform animation—the ball arcs through space rather than moving in straight lines.
293
+
294
+ ### Scoped Variables for Dynamic Behavior (Jhey)
295
+ CSS custom properties respect scope, enabling powerful patterns:
296
+ ```css
297
+ .item { --delay: 0; animation-delay: calc(var(--delay) * 100ms); }
298
+ .item:nth-child(1) { --delay: 0; }
299
+ .item:nth-child(2) { --delay: 1; }
300
+ .item:nth-child(3) { --delay: 2; }
301
+ ```
302
+
303
+ Use scoped variables to create varied behavior from a single animation definition.
304
+
305
+ ---
306
+
307
+ ## 8. 3D CSS (Jhey)
308
+
309
+ ### Think in Cuboids
310
+ > "Think in cubes instead of boxes" — Jhey Tompkins
311
+
312
+ Complex 3D scenes are assemblies of cube-shaped elements (like LEGO). Decompose any 3D object into cuboids.
313
+
314
+ ### Essential Setup
315
+ ```css
316
+ .scene {
317
+ transform-style: preserve-3d;
318
+ perspective: 1000px;
319
+ }
320
+ ```
321
+
322
+ ### Responsive 3D
323
+ Use CSS variables for dimensions and `vmin` units:
324
+ ```css
325
+ .cube {
326
+ --size: 10vmin;
327
+ width: var(--size);
328
+ height: var(--size);
329
+ }
330
+ ```
331
+
332
+ ---
333
+
334
+ ## 9. Clip-Path Animations (Emil)
335
+
336
+ ### Why clip-path?
337
+ - Hardware-accelerated rendering
338
+ - No layout shifts
339
+ - No additional DOM elements needed
340
+ - Smoother than width/height animations
341
+
342
+ ### Basic Syntax
343
+ ```css
344
+ clip-path: inset(top right bottom left);
345
+ clip-path: circle(radius at x y);
346
+ clip-path: polygon(coordinates);
347
+ ```
348
+
349
+ ### Image Reveal Effect
350
+ ```css
351
+ .reveal {
352
+ clip-path: inset(0 0 100% 0); /* Hidden */
353
+ animation: reveal 1s forwards cubic-bezier(0.77, 0, 0.175, 1);
354
+ }
355
+
356
+ @keyframes reveal {
357
+ to { clip-path: inset(0 0 0 0); } /* Fully visible */
358
+ }
359
+ ```
360
+
361
+ ### Tab Transitions
362
+ Duplicate tab lists with different styling. Animate the overlay's clip-path to reveal only the active tab—creates smooth color transitions without timing issues.
363
+
364
+ ### Scroll-Driven with clip-path
365
+ ```javascript
366
+ const clipPathY = useTransform(scrollYProgress, [0, 1], ["100%", "0%"]);
367
+ const motionClipPath = useMotionTemplate`inset(0 0 ${clipPathY} 0)`;
368
+ ```
369
+
370
+ ### Text Mask Effect
371
+ Stack elements with complementary clip-paths:
372
+ ```css
373
+ .top { clip-path: inset(0 0 50% 0); } /* Shows top half */
374
+ .bottom { clip-path: inset(50% 0 0 0); } /* Shows bottom half */
375
+ ```
376
+ Adjust values on mouse interaction for seamless transitions.
377
+
378
+ ---
379
+
380
+ ## 10. Button & Interactive Feedback (Emil)
381
+
382
+ ### Scale on Press
383
+ Add immediate tactile feedback:
384
+ ```css
385
+ button:active {
386
+ transform: scale(0.97);
387
+ }
388
+ ```
389
+
390
+ ### Don't Animate from scale(0)
391
+ ```jsx
392
+ // BAD: Unnatural motion
393
+ initial={{ scale: 0 }}
394
+
395
+ // GOOD: Natural, gentle motion
396
+ initial={{ scale: 0.9, opacity: 0 }}
397
+ animate={{ scale: 1, opacity: 1 }}
398
+ ```
399
+
400
+ ### Tooltip Delay Pattern
401
+ First tooltip in a group: delay + animation. Subsequent tooltips: instant.
402
+ ```css
403
+ [data-instant] {
404
+ transition-duration: 0ms;
405
+ }
406
+ ```
407
+
408
+ ### Blur as a Bridge
409
+ When state transitions aren't smooth enough, add blur to mask imperfections:
410
+ ```css
411
+ .transitioning {
412
+ filter: blur(2px);
413
+ }
414
+ ```
415
+
416
+ ---
417
+
418
+ ## 11. CSS Transitions vs Keyframes (Emil)
419
+
420
+ ### Interruptibility Problem
421
+ CSS keyframes can't be interrupted mid-animation. When users rapidly trigger actions, elements "jump" to new positions rather than smoothly retargeting.
422
+
423
+ **Solution**: Use CSS transitions with state-driven classes:
424
+ ```jsx
425
+ useEffect(() => {
426
+ setMounted(true);
427
+ }, []);
428
+ ```
429
+
430
+ ```css
431
+ .element {
432
+ transform: translateY(100%);
433
+ transition: transform 400ms ease;
434
+ }
435
+ .element.mounted {
436
+ transform: translateY(0);
437
+ }
438
+ ```
439
+
440
+ ### Direct Style Updates for Performance
441
+ CSS variables cause style recalculation across all children. For frequent updates (drag operations), update styles directly:
442
+
443
+ ```javascript
444
+ // BAD: CSS variable (expensive cascade)
445
+ element.style.setProperty('--drag-y', `${y}px`);
446
+
447
+ // GOOD: Direct style (no cascade)
448
+ element.style.transform = `translateY(${y}px)`;
449
+ ```
450
+
451
+ ### Momentum-Based Dismissal
452
+ Use velocity (distance / time) instead of distance thresholds:
453
+ ```javascript
454
+ const velocity = dragDistance / elapsedTime;
455
+ if (velocity > 0.11) dismiss();
456
+ ```
457
+
458
+ Fast, short gestures should work—users shouldn't need to drag far.
459
+
460
+ ### Damping for Natural Boundaries
461
+ When dragging past boundaries, reduce movement progressively. Things in real life slow down before stopping.
462
+
463
+ ---
464
+
465
+ ## 12. Spring Physics (Emil)
466
+
467
+ ### Key Parameters
468
+ | Parameter | Effect |
469
+ |-----------|--------|
470
+ | **Stiffness** | How quickly spring reaches target (higher = faster) |
471
+ | **Damping** | How quickly oscillations settle (higher = less bounce) |
472
+ | **Mass** | Weight of object (higher = more momentum) |
473
+
474
+ ### Spring for Mouse Position
475
+ ```javascript
476
+ const springConfig = { stiffness: 300, damping: 30 };
477
+ const x = useSpring(mouseX, springConfig);
478
+ const y = useSpring(mouseY, springConfig);
479
+ ```
480
+
481
+ Use `useSpring` for any value that should interpolate smoothly rather than snap—nothing in the real world changes instantly.
482
+
483
+ ### Interruptibility
484
+ Great animations can be interrupted mid-play:
485
+ - Framer Motion supports interruption natively
486
+ - CSS transitions allow smooth interruption before completion
487
+ - Test by clicking rapidly—animations should blend, not queue
488
+
489
+ ---
490
+
491
+ ## 12. Origin-Aware Animations (Emil)
492
+
493
+ Animations should originate from their logical source:
494
+
495
+ ```css
496
+ /* Dropdown from button should expand from button, not center */
497
+ .dropdown {
498
+ transform-origin: top center;
499
+ }
500
+ ```
501
+
502
+ **Component library support:**
503
+ - Base UI: `--transform-origin` CSS variable
504
+ - Radix UI: `--radix-dropdown-menu-content-transform-origin`
505
+
506
+ ---
507
+
508
+ ## 13. Scroll-Driven Animations (Jhey)
509
+
510
+ ### The Core Problem
511
+ Scroll-driven animations are tied to scroll **speed**. If users scroll slowly, animations play slowly. This feels wrong for most UI—you want animations to trigger at a scroll position, not be controlled by scroll speed.
512
+
513
+ ### Duration Control Pattern
514
+ Use two coordinated animations:
515
+ 1. **Trigger animation**: Scroll-driven, toggles a custom property when element enters view
516
+ 2. **Main animation**: Traditional duration-based, activated via Style Query
517
+
518
+ This severs the connection between scroll speed and animation timing—the animation runs over a fixed duration once triggered, regardless of how fast the user scrolled.
519
+
520
+ ### Progressive Enhancement
521
+ Always provide fallbacks:
522
+ ```javascript
523
+ // IntersectionObserver fallback for browsers without scroll-driven animation support
524
+ if (!CSS.supports('animation-timeline', 'scroll()')) {
525
+ // Use IntersectionObserver instead
526
+ }
527
+ ```