react-animations
React Animations
Production-quality animations in React using Framer Motion (primary), React Spring (physics), GSAP (timelines), and CSS/Tailwind (simple cases).
Library Decision Table
| Use Case | Library | Why |
|---|---|---|
| Entrance/exit animations | Framer Motion | AnimatePresence handles unmount |
| Shared element transitions | Framer Motion | layoutId |
| Physics-based (spring, bounce) | Framer Motion or React Spring | spring config |
| Complex timelines / sequences | GSAP | Timeline API |
| Scroll-triggered | Framer Motion (whileInView / useScroll) | Built-in scroll hooks |
| Simple hover/focus states | CSS Tailwind | No JS needed |
| Drag and drop | Framer Motion | Built-in gesture support |
| SVG path animations | GSAP or Framer Motion | Both support SVG |
| Imperative / programmatic | Framer Motion useAnimate | Modern imperative API |
Core Principles
Matt Perry (Framer Motion creator): "Animations should be declared, not imperatively managed. Describe the target state — the library handles the rest." Sarah Drasner: "Animation is not decoration — it's communication. Every motion should serve a purpose."
- CSS for simple, JS for complex — if Tailwind
transitionworks, use it; don't add Framer Motion for hover states - Only animate composited properties —
transformandopacity; neverwidth,height,top,left(causes reflow) - AnimatePresence wraps conditional renders — without it, exit animations are skipped
- Variants for coordinated animations — define animation states as objects outside the component, not inline values
- layoutId for shared element transitions — Framer Motion handles the interpolation between positions
- useMotionValue for gesture-driven — don't use useState for values that drive animations
- 60fps budget — keep animation logic out of render cycle; use transforms
Key Framer Motion Patterns
Basic Animate
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
Gesture States (whileHover, whileTap)
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
/>
Variants with Children Stagger
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const item = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map((i) => (
<motion.li key={i} variants={item} />
))}
</motion.ul>
AnimatePresence with Exit
The mode prop controls how entering/exiting elements interact:
"sync"(default) — enter and exit happen simultaneously"wait"— exit completes before enter starts (good for page transitions)"popLayout"— exiting element is removed from layout flow immediately
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
/>
)}
</AnimatePresence>
whileInView for Scroll-Triggered Animations
The simplest approach — no scroll hooks needed:
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5 }}
/>
Use viewport.once: true so the animation doesn't replay on scroll back.
useScroll + useTransform for Parallax
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
<motion.div style={{ y }} />
layoutId Shared Element
// In list view
<motion.div layoutId={`card-${id}`}>
<Thumbnail />
</motion.div>
// In detail view
<motion.div layoutId={`card-${id}`}>
<FullImage />
</motion.div>
Drag with Constraints
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2}
whileDrag={{ scale: 1.1 }}
/>
useAnimate — Imperative Animations (v11+)
Prefer useAnimate over useAnimation for programmatic sequences. It's scoped to a ref and works with any selector within that scope:
const [scope, animate] = useAnimate();
const handleClick = async () => {
await animate(scope.current, { scale: 1.2 }, { duration: 0.2 });
await animate(scope.current, { scale: 1 }, { duration: 0.1 });
};
<div ref={scope}>
<button onClick={handleClick}>Click me</button>
</div>
MotionConfig — Global Animation Settings
Wrap your app (or a subtree) to set defaults like reduced motion or spring presets:
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
reducedMotion="user" automatically disables animations for users with OS-level "reduce motion" preferences. This is the easiest way to handle accessibility at scale.
LazyMotion — Bundle Size Optimization
For production apps, replace motion with LazyMotion + m to code-split the animation engine:
import { LazyMotion, domAnimation, m } from 'framer-motion';
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
Use domMax instead of domAnimation if you need drag or layout animations.
Game UI Patterns
Health Bar Smooth Tweening
const motionWidth = useMotionValue(current / max);
const springWidth = useSpring(motionWidth, { stiffness: 200, damping: 30 });
useEffect(() => {
motionWidth.set(Math.max(0, Math.min(1, current / max)));
}, [current, max]);
<motion.div style={{ scaleX: springWidth, transformOrigin: 'left' }} />
Damage Numbers Floating Up
<motion.span
initial={{ opacity: 1, y: 0 }}
animate={{ opacity: 0, y: -60 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
onAnimationComplete={onComplete}
>
-{damage}
</motion.span>
Card Flip (rotateY)
<motion.div animate={{ rotateY: isFlipped ? 180 : 0 }} style={{ perspective: 1000 }}>
<div style={{ backfaceVisibility: 'hidden' }}>{front}</div>
<div style={{ backfaceVisibility: 'hidden', rotateY: 180 }}>{back}</div>
</motion.div>
Screen Shake (useAnimate)
const [scope, animate] = useAnimate();
const shake = async () => {
await animate(scope.current, { x: [0, -10, 10, -10, 10, 0] }, { duration: 0.4 });
};
<div ref={scope}>{children}</div>
Menu Slide-In / Slide-Out
<AnimatePresence>
{isOpen && (
<motion.nav
initial={{ x: -300 }}
animate={{ x: 0 }}
exit={{ x: -300 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
</AnimatePresence>
Inventory Item Drop (Spring Physics)
<motion.div
initial={{ y: -200, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ type: 'spring', stiffness: 400, damping: 15 }}
/>
Accessibility
Always respect the user's motion preferences. Two approaches:
1. MotionConfig (recommended for apps) — wraps your entire component tree:
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
2. useReducedMotion hook — for component-level control:
const shouldReduceMotion = useReducedMotion();
<motion.div
animate={{ opacity: 1, y: shouldReduceMotion ? 0 : -20 }}
/>
Performance
- Animate only
transformandopacity— GPU-composited, no layout/paint triggered will-change: transformfor elements that always animate (promotes to own layer)- Use
motion.create(Component)to animate custom components without wrapper divs layoutprop triggers automatic layout animations — expensive on large DOM trees; scope to smallest possible subtree- Prefer
useMotionValueoveruseStatefor animation-driving values — motion values don't trigger re-renders LazyMotionwithdomAnimationsaves ~15kb gzip vs the full bundle
Setup
bun add framer-motion
Zero-config — no providers required. Import and use motion.div directly.
For global accessibility handling, wrap your app root:
import { MotionConfig } from 'framer-motion';
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
Boilerplate
boilerplate/motion-components.tsx — ready-to-use components: FadeIn, SlideIn, ScaleIn, StaggerList, FloatingNumber, HealthBar, AnimatedCard
templates/animation-variants.ts — reusable variant objects and spring transition presets
Cross-References
vercel-react-best-practices— React performance patternsui-ux-game— game HUD and UI patternsfrontend-design— component design and Tailwind patterns
Pitfalls & Anti-Patterns
- Animating layout-triggering properties (
width,height,top,left) — usescaleX/scaleYor thelayoutprop instead - Forgetting AnimatePresence when using
exitprop — exit animations silently skip without the wrapper - Creating motion values in render — use
useMotionValuehook; creating in render causes memory leaks - Using CSS
transitionAND Framer Motion on same element — they conflict; pick one - Over-animating — every interaction animated is sensory overload; animate to communicate, not to decorate
- Animating on mount without
initial— component flashes before animating; always setinitial - Using
useAnimation— deprecated; useuseAnimatefor imperative animations instead - Large
layoutanimations —layoutprop on deeply nested trees causes expensive recalculations; scope to smallest possible subtree - Skipping accessibility — always use
MotionConfig reducedMotion="user"oruseReducedMotion
Sources
- Framer Motion documentation — https://motion.dev
- Matt Perry — Framer Motion creator, API design talks
- Sarah Drasner — "SVG Animations", animation design patterns
- GSAP documentation — https://gsap.com