A practical guide to building accessible, performant animations with Framer Motion. Covers when to animate, what to animate, performance budgets, accessibility requirements, and debugging techniques.
| Situation | Why | Pattern |
|---|---|---|
| Content enters the viewport | Draws attention to new content | ScrollReveal with fadeUp |
| User navigates between pages | Provides spatial continuity | PageTransition with slide |
| Data loads asynchronously | Reduces perceived wait time | SkeletonLoader → ContentSwap |
| User interacts with an element | Confirms the interaction was received | whileHover + whileTap |
| A value changes | Shows the change, not just the result | AnimatedCounter |
| Items are added/removed | Prevents jarring layout shifts | AnimatePresence + exit variants |
| State toggles | Shows the transition between states | layout animation |
| Situation | Why |
|---|---|
| The user is typing | Animations near text input are distracting |
| Form validation errors appear | Users need to read errors, not watch them bounce |
| Data is already visible | Re-animating visible content feels like lag |
| Multiple elements need attention | Too many simultaneous animations cause chaos |
| The action is destructive | Delete confirmations should feel deliberate, not flashy |
| Network requests are in-flight | Don't animate loading states that may flash briefly |
If an animation takes longer than 3 seconds to complete (including stagger), it's too long. Users lose patience after 3 seconds. The ideal animation:
Is the element entering the viewport for the first time?
├── YES → Is it triggered by scroll?
│ ├── YES → ScrollReveal component
│ └── NO → Entrance variant (fadeUp, scaleUp, etc.)
└── NO → Is the element responding to user interaction?
├── YES → Is it a hover/tap?
│ ├── YES → Hover variant (lift, press, grow, etc.)
│ └── NO → useMicroInteraction hook
└── NO → Is the element leaving?
├── YES → Exit variant + AnimatePresence
└── NO → Is it a layout change?
├── YES → layout prop + LayoutGroup
└── NO → You probably don't need animation here
| Context | Recommended Pattern | Avoid |
|---|---|---|
| Marketing landing page | ScrollReveal, Parallax, StaggerList | Over-animating — let content breathe |
| Dashboard / data app | PageTransition, AnimatedCounter, SkeletonLoader | Parallax (too playful for data UI) |
| E-commerce | fadeUp on products, lift on cards, ContentSwap | Slow transitions (users are goal-oriented) |
| Blog / documentation | ScrollReveal (subtle), fadeUp only | Stagger (text should appear all at once) |
| Mobile app | Reduced distances, faster springs, press over lift | Parallax (scroll jank risk on mobile) |
| Portfolio / creative | blurIn, clipReveal, morph page transitions | Conservative patterns (this is where you show off) |
At 60fps, each frame has 16.67ms. Your animation logic must complete within this budget:
16ms total frame budget
├── 2-4ms JavaScript (animation calculations)
├── 2-4ms Style recalculation
├── 2-4ms Layout (if properties change layout)
├── 2-4ms Paint (if properties need repainting)
└── 2-4ms Compositing (GPU layer management)
| Tier | Properties | Cost | GPU Composited? |
|---|---|---|---|
| Free | transform (translate, scale, rotate) | Composite only | Yes |
| Free | opacity | Composite only | Yes |
| Cheap | filter (blur, brightness) | Paint + Composite | Partially |
| Expensive | box-shadow | Paint + Composite | No |
| Very Expensive | width, height, padding, margin | Layout + Paint + Composite | No |
| Catastrophic | top, left, right, bottom | Layout + Paint + Composite | No |
1. Only animate transform and opacity for the smoothest results
2. Use will-change: transform to hint the browser (the variants in this pack do this automatically)
3. Limit will-change to 3-5 elements — too many promoted layers waste GPU memory
4. Avoid animating during scroll unless using useScroll() (passive, RAF-based)
5. Test on low-end devices — your MacBook Pro is not representative
// In Chrome DevTools:
// 1. Open Performance panel
// 2. Enable "Screenshots" and "Web Vitals"
// 3. Click Record, trigger your animation, stop
// 4. Look for:
// - Long frames (red bars above 16ms)
// - Layout shifts (yellow "Layout" blocks)
// - Paint events (green blocks — should be rare during animation)
// Programmatically:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 16) {
console.warn(`Long frame: ${entry.duration.toFixed(1)}ms`, entry);
}
}
});
observer.observe({ type: 'longtask', buffered: true });This is not optional. Approximately 25-35% of users have motion sensitivity to some degree. Vestibular disorders can cause nausea, dizziness, and migraines from screen motion.
Every animation in this pack respects prefers-reduced-motion: reduce automatically. Here's what happens:
| Full Motion | Reduced Motion |
|---|---|
| Slide + fade entrance | Opacity-only fade (200ms) |
| Parallax scroll effects | Static content (no movement) |
| Staggered list animation | Quick sequential opacity fade |
| Spring-based hover | Opacity change (no transform) |
| Page slide transition | Crossfade only |
| Animated counter | Shows final value immediately |
| Shimmer skeleton | Slow pulse (no directional movement) |
macOS: System Settings → Accessibility → Display → Reduce motion
Windows: Settings → Ease of Access → Display → Show animations → Off
Chrome DevTools: Rendering → Emulate CSS media feature → prefers-reduced-motion: reduce
The ReducedMotionProvider in this pack satisfies all three criteria when wrapped around your app root.
Springs use physics simulation (stiffness, damping, mass) instead of fixed duration + easing curve.
Use springs for:
Why: Springs handle interruption gracefully. If a spring animation is in-flight and you change the target, it smoothly redirects. Tweens jump to the start of the new animation.
Tweens use fixed duration + easing curve (like CSS transitions).
Use tweens for:
| Feel | Stiffness | Damping | Mass | Duration (approx) |
|---|---|---|---|---|
| Snappy (buttons) | 400 | 30 | 1.0 | ~200ms |
| Bouncy (playful) | 400 | 15 | 0.8 | ~400ms |
| Gentle (pages) | 120 | 20 | 1.2 | ~600ms |
| Stiff (instant) | 600 | 35 | 0.5 | ~100ms |
| Heavy (dramatic) | 100 | 25 | 2.0 | ~800ms |
| Wobbly (fun) | 350 | 10 | 0.8 | ~600ms |
Use springFromBehavior() from easing.ts to calculate these from desired duration and bounce.
// BAD: Animating width causes layout thrashing
<motion.div animate={{ width: isOpen ? 300 : 0 }} />
// GOOD: Animate scaleX instead (GPU composited)
<motion.div animate={{ scaleX: isOpen ? 1 : 0 }} style={{ transformOrigin: 'left' }} />
// GOOD: If you must animate width, use layout animation
<motion.div layout style={{ width: isOpen ? 300 : 0 }} />// BAD: Every card animates independently on mount
{cards.map(card => (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{card.title}
</motion.div>
))}
// GOOD: Use stagger so they animate sequentially
<StaggerList items={cards} staggerDelay={0.06} renderItem={...} />// BAD: Element unmounts instantly — exit animation never plays
{isVisible && <motion.div exit={{ opacity: 0 }}>Content</motion.div>}
// GOOD: AnimatePresence keeps element mounted during exit animation
<AnimatePresence>
{isVisible && (
<motion.div key="content" exit={{ opacity: 0 }}>Content</motion.div>
)}
</AnimatePresence>// BAD: No key — AnimatePresence can't track which element is leaving
<AnimatePresence>
{items.map(item => <motion.div>{item.name}</motion.div>)}
</AnimatePresence>
// GOOD: Unique key lets AnimatePresence track each element
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id}>{item.name}</motion.div>
))}
</AnimatePresence>// BAD: No reduced motion consideration
<motion.div animate={{ x: 100, rotate: 360 }} />
// GOOD: Use variants from this pack — they include reduced fallbacks
import { fadeUp } from './variants/entrance';
const prefersReduced = useReducedMotion();
<motion.div {...(prefersReduced ? fadeUp.reduced : fadeUp)} />1. Slow down animations: Rendering → "Animation slow-down" slider
2. See layer composition: Layers panel → shows which elements are GPU-composited
3. Detect layout shifts: Performance panel → look for yellow "Layout" events during animation
4. Monitor paint: Rendering → "Paint flashing" → green highlights show repaints
// Log animation lifecycle
<motion.div
onAnimationStart={() => console.log('Animation started')}
onAnimationComplete={() => console.log('Animation completed')}
animate={{ opacity: 1 }}
/>
// Inspect motion values
import { useMotionValue, useTransform } from 'framer-motion';
const x = useMotionValue(0);
x.on('change', (latest) => console.log('x:', latest));| Symptom | Cause | Fix |
|---|---|---|
| Animation stutters on first run | Browser creating new GPU layer | Add will-change: transform to the element's idle state |
| Exit animation doesn't play | Missing AnimatePresence wrapper | Wrap conditional rendering in |
| Stagger feels sluggish | Too many items with default delay | Use safeStaggerDelay() to cap total time |
| Spring oscillates too long | Damping too low | Increase damping or use restDelta option |
| Animation replays on every render | initial prop without key | Add stable key prop or use initial={false} |
| Layout shift when element appears | Element has fixed dimensions | Use layout prop for smooth size transitions |
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| CSS transforms | 36+ | 16+ | 9+ | 12+ |
| Intersection Observer | 51+ | 55+ | 12.1+ | 15+ |
| prefers-reduced-motion | 74+ | 63+ | 10.1+ | 79+ |
| Web Animations API | 36+ | 48+ | 13.1+ | 79+ |
| will-change | 36+ | 36+ | 9.1+ | 79+ |
| filter (blur, etc.) | 53+ | 35+ | 9.1+ | 79+ |
| clip-path (inset) | 55+ | 54+ | 13.1+ | 79+ |
All features used in this pack are supported in all modern browsers (released after 2020).
For IE11 support (not recommended), you would need:
Get the full Animation & Effects Pack and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.