/advanced-motion-easing
When to Use
- Implementing spring-based UI transitions (not CSS ease-in-out)
- Building drag-and-drop with inertia
- Scroll-driven animations that feel native
- Choreographing staggered list entrances
- Integrating Lottie / Rive animations with code control
Do NOT use for
- Static pages with no interaction
- Transitions < 150ms (imperceptible — skip animation entirely)
Spring Config Reference (framer/motion + react-spring)
// Named presets — use these before custom tuning
const springs = {
snappy: { stiffness: 400, damping: 30, mass: 1 }, // UI micro
default: { stiffness: 260, damping: 25, mass: 1 }, // standard nav
gentle: { stiffness: 120, damping: 20, mass: 1 }, // modals, panels
wobbly: { stiffness: 180, damping: 12, mass: 1 }, // playful UIs
slow: { stiffness: 60, damping: 15, mass: 1 }, // hero reveals
}
// framer-motion usage
<motion.div animate={{ y: 0 }} transition={springs.snappy} />
// react-spring usage
const { y } = useSpring({ y: 0, config: springs.gentle })
Custom Easing Curves (css-spring-easing + GSAP)
// css-spring-easing — generates linear() from spring params
import { createEasing } from 'css-spring-easing'
const [easing, duration] = createEasing({ stiffness: 260, damping: 25 })
// → "linear(0, 0.012, 0.044...)" — native CSS, no JS runtime
element.animate({ transform: ['translateY(20px)', 'translateY(0)'] }, {
duration,
easing,
fill: 'both',
})
GPU Compositing Rules (framer/motion + GSAP guidelines)
✅ Animate: transform (translate, scale, rotate), opacity
✅ Use will-change: transform sparingly — only during animation
✅ Use translateZ(0) or translate3d to force GPU layer
❌ Never animate: width, height, top, left, margin, padding
→ triggers layout reflow on every frame
❌ Never animate: background-color, box-shadow
→ triggers paint; use opacity + pseudo-element trick instead
// Correct shadow animation
.card::after {
opacity: 0;
box-shadow: 0 8px 24px rgba(0,0,0,.2);
transition: opacity 200ms;
}
.card:hover::after { opacity: 1; }
Scroll-Driven Animations (CSS native)
/* CSS Scroll-driven (Chrome 115+) — zero JS */
@keyframes reveal {
from { opacity: 0; translate: 0 40px; }
to { opacity: 1; translate: 0 0; }
}
.card {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 40%;
}
/* Sticky progress bar */
#progress {
position: fixed;
transform-origin: left;
animation: grow linear;
animation-timeline: scroll(root block);
}
@keyframes grow { to { transform: scaleX(1); } }
Stagger Choreography (anime.js)
import anime from 'animejs'
// Max stagger total = 400ms (beyond that feels broken)
anime({
targets: '.list-item',
opacity: [0, 1],
translateY: [16, 0],
easing: 'spring(1, 80, 10, 0)',
delay: anime.stagger(50, { start: 0 }), // 50ms × n items
duration: 350,
})
// 8 items × 50ms = 400ms — at limit; more items → reduce delay
// Rule: (n_items × stagger_ms) + duration ≤ 800ms total
Lottie Integration (lottie-web)
import lottie from 'lottie-web'
const anim = lottie.loadAnimation({
container: document.getElementById('lottie'),
renderer: 'svg', // SVG for quality; canvas for 60fps perf
loop: false,
autoplay: false,
path: '/animations/success.json',
})
// Control from code (theatre.js pattern)
anim.addEventListener('DOMLoaded', () => {
anim.goToAndStop(0, true) // start at frame 0
})
function playOnHover() { anim.play() }
function reset() { anim.goToAndStop(0, true) }
Reduced-Motion (mandatory — framer/motion hook)
import { useReducedMotion } from 'framer-motion'
function AnimatedCard() {
const reduceMotion = useReducedMotion()
return (
<motion.div
initial={{ opacity: reduceMotion ? 1 : 0, y: reduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={reduceMotion ? { duration: 0 } : springs.default}
/>
)
}
// CSS fallback (always include both)
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Anti-Fake-Pass Checklist
❌ Animating width/height/top/left (layout thrash)
❌ Stagger total > 800ms (items beyond 8 at 50ms stagger)
❌ will-change: transform left on permanently (memory leak)
❌ No prefers-reduced-motion guard
❌ Lottie using canvas renderer for < 60fps SVG-quality use case
❌ CSS transition on box-shadow (use opacity + ::after trick)
❌ Spring damping < 10 (infinite oscillation)