The precise values, curves, and rules behind the review. Cite these in findings instead of approximating. Distilled from Emil Kowalski's design engineering philosophy.
Never animate keyboard-initiated actions — they repeat hundreds of times daily; animation makes them feel slow and disconnected. (Raycast has no open/close animation — correct for something used hundreds of times a day.)
Valid purposes for motion: spatial consistency, state indication, explanation, feedback, preventing jarring change. "It looks cool" on a frequently-seen element is not valid.
Rule: UI animations stay under 300ms. A 180ms dropdown feels more responsive than a 400ms one. Faster spinners make load feel faster (same actual time). Instant tooltips after the first (skip delay + animation) make a toolbar feel faster.
Feel natural because they simulate physics; no fixed duration — they settle on parameters. Use for: drag with momentum, "alive" elements (Dynamic Island), interruptible gestures, decorative mouse-tracking.
Keep bounce subtle (0.1–0.3); avoid bounce in most UI — reserve for drag-to-dismiss and playful interactions. Springs maintain velocity when interrupted (keyframes restart from zero), so they're ideal for gestures users may reverse mid-motion.
Mouse interactions: interpolate with useSpring rather than tying value directly to mouse position (direct = artificial, no momentum). Only do this when the motion is decorative.
CSS transitions can be interrupted and retargeted mid-animation; keyframes restart from zero. For anything triggered rapidly (toasts being added, toggles), transitions are smoother.
/* Interruptible — good for dynamic UI */.toast { transition: transform 400ms ease; }/* Not interruptible — avoid for dynamic UI */@keyframes slideIn { from { transform: translateY(100%); } to { transform: translateY(0); } }
Only animate transform and opacity — they skip layout/paint and run on the GPU. padding/margin/height/width/top/left trigger all three rendering steps.
Don't drive child transforms via a CSS variable on the parent — it recalcs styles for all children. Set transform directly on the element.
element.style.setProperty('--swipe-amount', `${d}px`); // bad: recalc on all childrenelement.style.transform = `translateY(${d}px)`; // good: only this element
Framer Motion shorthands are NOT hardware-accelerated.x/y/scale run on the main thread via rAF and drop frames under load. Use the full transform string:
CSS animations beat JS under load — they run off the main thread; rAF-based animations stutter while the browser loads/scripts/paints. Use CSS for predetermined motion, JS for dynamic/interruptible.
WAAPI gives JS control with CSS performance (hardware-accelerated, interruptible, no library):
translate percentages are relative to the element's own size — translateY(100%) moves by the element's height regardless of dimensions (how Sonner/Vaul position toasts/drawers). Prefer over hardcoded px.
scale() scales children too (font, icons, content) — a feature for press feedback.
3D: rotateX/Y + transform-style: preserve-3d for depth/orbit/flip without JS.
clip-path: inset(t r b l) is a powerful animation tool: each value eats in from that side. Uses: reveal-on-scroll (inset(0 0 100% 0) → inset(0 0 0 0)), hold-to-delete overlay, seamless tab color transitions (duplicate + clip the active copy), comparison sliders.
Momentum dismissal: don't require crossing a distance threshold — compute velocity (Math.abs(distance)/elapsedMs); dismiss if > ~0.11. A flick should be enough.
Damping at boundaries: dragging past a natural edge moves less the further you go (real things slow before stopping).
Pointer capture once dragging starts, so it continues when the pointer leaves bounds.
Multi-touch protection: ignore extra touch points after the drag begins (if (isDragging) return) — prevents jumps.
Friction over hard stops — allow over-drag with rising resistance rather than an invisible wall.
When a crossfade shows two overlapping states despite tuning easing/duration, add subtle filter: blur(2px) during the transition to blend them into one perceived transformation. Keep blur < 20px (heavy blur is expensive, especially Safari).
Match motion to the component's personality: playful can be bouncier; a professional dashboard should be crisp and fast. Sonner feels right partly because easing, duration, design, and even the name are in harmony — slightly slower, ease rather than ease-out, to feel elegant. Opacity + height in entering/exiting lists is trial and error; there's no formula — adjust until it feels right.