@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.
- package/SKILL.md +334 -0
- package/audit-checklist.md +137 -0
- package/package.json +18 -0
- package/references/accessibility.md +52 -0
- package/references/common-mistakes.md +158 -0
- package/references/emil-kowalski.md +355 -0
- package/references/jakub-krehel.md +317 -0
- package/references/jhey-tompkins.md +367 -0
- package/references/performance.md +82 -0
- package/references/philosophy.md +108 -0
- package/references/technical-principles.md +527 -0
|
@@ -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.
|