Framer Motion
Platform: Web only. For mobile animations, see the react-native-reanimated skill.
Overview
Animation patterns for React using Framer Motion 12.x. Provides declarative animations, gesture handling, layout transitions, and page animations with performance and accessibility built-in.
Install: pnpm add framer-motion
Workflows
Adding animations:
-
Import motion component: import { motion } from 'framer-motion'
-
Replace element with motion variant: <div> → <motion.div>
-
Add animation props: initial, animate, transition
-
Test with reduced motion enabled
-
Verify 60fps performance in DevTools
Complex sequences:
-
Define variants object with named states
-
Apply variants to parent and children
-
Use orchestration props: staggerChildren, delayChildren
-
Wrap with AnimatePresence if unmounting
-
Add accessibility fallbacks
Animation Primitives
Basic Motion Components
import { motion } from 'framer-motion';
// Simple fade-in <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }}
Content </motion.div>
// Slide up with fade <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, ease: 'easeOut' }}
Content </motion.div>
Variants for Complex Animations
const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1, delayChildren: 0.2 } }, exit: { opacity: 0, transition: { duration: 0.15 } } };
const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 } };
<motion.ul variants={containerVariants} initial="hidden" animate="visible" exit="exit"> {items.map(item => ( <motion.li key={item.id} variants={itemVariants}> {item.name} </motion.li> ))} </motion.ul>
Transitions
Standard Timings
// Use consistent timing across app const timing = { fast: 0.15, // Micro-interactions normal: 0.3, // Default animations slow: 0.5, // Page transitions stagger: 0.05 // Between items };
// Duration and easing <motion.div animate={{ x: 100 }} transition={{ duration: timing.normal, ease: 'easeInOut' }} />
// Spring physics (preferred for natural motion) <motion.div animate={{ scale: 1.2 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }} />
// Keyframes <motion.div animate={{ scale: [1, 1.2, 1] }} transition={{ duration: 0.5, times: [0, 0.5, 1] }} />
// Repeat <motion.div animate={{ rotate: 360 }} transition={{ duration: 2, repeat: Infinity, repeatType: 'loop' }} />
Gestures
Hover, Tap, Focus
<motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} whileFocus={{ outline: '2px solid blue' }} transition={{ duration: 0.15 }}
Click me </motion.button>
// Complex hover state <motion.div initial="rest" whileHover="hover" variants={{ rest: { scale: 1, boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }, hover: { scale: 1.02, boxShadow: '0 8px 16px rgba(0,0,0,0.15)' } }}
Card content </motion.div>
Drag with Constraints
import { useRef } from 'react';
const constraintsRef = useRef(null);
<div ref={constraintsRef} style={{ width: 400, height: 400 }}> <motion.div drag dragConstraints={constraintsRef} dragElastic={0.1} whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
Drag me
</motion.div> </div>
// Drag along single axis <motion.div drag="x" dragConstraints={{ left: -100, right: 100 }}> Slide horizontal </motion.div>
Layout Animations
Automatic Layout Animation
// Auto-animates position/size changes <motion.div layout> {expanded ? <FullContent /> : <Summary />} </motion.div>
// Shared element transitions <motion.div layoutId="card-123"> <motion.img layoutId="card-image-123" src={image} /> </motion.div>
// Coordinate sibling animations import { LayoutGroup } from 'framer-motion';
<LayoutGroup> {items.map(item => ( <motion.div key={item.id} layout> {item.content} </motion.div> ))} </LayoutGroup>
Page Transitions
AnimatePresence for Exit Animations
import { AnimatePresence } from 'framer-motion';
// Single element <AnimatePresence mode="wait"> {isVisible && ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > Content </motion.div> )} </AnimatePresence>
// Route transitions (with React Router) import { useLocation } from 'react-router-dom';
const location = useLocation();
<AnimatePresence mode="wait" initial={false}> <motion.div key={location.pathname} initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} transition={{ duration: 0.3 }}
<Routes location={location}>
{/* routes */}
</Routes>
</motion.div> </AnimatePresence>
Stagger Patterns
// Parent orchestration const listVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1, delayChildren: 0.2 } } };
const itemVariants = { hidden: { opacity: 0, x: -20 }, visible: { opacity: 1, x: 0 } };
<motion.ul variants={listVariants} initial="hidden" animate="visible"> {items.map(item => ( <motion.li key={item.id} variants={itemVariants}> {item.name} </motion.li> ))} </motion.ul>
// Custom stagger with useAnimate import { useAnimate, stagger } from 'framer-motion';
const [scope, animate] = useAnimate();
useEffect(() => { animate('.item', { opacity: 1 }, { delay: stagger(0.05) }); }, []);
Performance
GPU-Accelerated Properties
// ✅ FAST: Only transform and opacity <motion.div animate={{ opacity: 1, scale: 1.2, x: 100, rotate: 45 }} />
// ❌ SLOW: Layout-affecting properties <motion.div animate={{ width: 300, // Triggers layout height: 200, // Triggers layout top: 50 // Triggers layout }} />
willChange Optimization
// Hint browser before expensive animations <motion.div style={{ willChange: 'transform' }} whileHover={{ scale: 1.1 }}
Content </motion.div>
// Auto willChange with layout animations <motion.div layout transition={{ layout: { duration: 0.3 } }}> Content </motion.div>
Accessibility
Reduced Motion Support
import { useReducedMotion } from 'framer-motion';
function AnimatedComponent() { const shouldReduceMotion = useReducedMotion();
return ( <motion.div initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: shouldReduceMotion ? 0 : 0.3 }} > Content </motion.div> ); }
// Disable animations completely const prefersReducedMotion = useReducedMotion();
<motion.div {...(prefersReducedMotion ? {} : { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.3 } })}
Content </motion.div>
Focus Management
// Maintain focus during animations <AnimatePresence> {isOpen && ( <motion.dialog initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} onAnimationComplete={() => { // Focus first input after enter animation dialogRef.current?.querySelector('input')?.focus(); }} > <form>...</form> </motion.dialog> )} </AnimatePresence>
Scroll Animations
useScroll and useInView
import { motion, useScroll, useTransform, useInView } from 'framer-motion'; import { useRef } from 'react';
// Scroll progress indicator function ScrollProgress() { const { scrollYProgress } = useScroll();
return ( <motion.div className="fixed top-0 left-0 right-0 h-1 bg-blue-600 origin-left" style={{ scaleX: scrollYProgress }} /> ); }
// Parallax effect function ParallaxSection() { const ref = useRef(null); const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] });
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
return ( <div ref={ref}> <motion.div style={{ y }}> Parallax content </motion.div> </div> ); }
// Trigger animation when element enters viewport function AnimateOnScroll({ children }: { children: React.ReactNode }) { const ref = useRef(null); const isInView = useInView(ref, { once: true, margin: "-100px" });
return ( <motion.div ref={ref} initial={{ opacity: 0, y: 50 }} animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }} transition={{ duration: 0.5 }} > {children} </motion.div> ); }
MotionConfig
Global Animation Settings
import { MotionConfig } from 'framer-motion';
// Apply global settings to all descendants function App() { return ( <MotionConfig reducedMotion="user" // Respect prefers-reduced-motion transition={{ duration: 0.3, ease: "easeOut" }} > <YourApp /> </MotionConfig> ); }
// Override transitions for a section function FastSection() { return ( <MotionConfig transition={{ duration: 0.15 }}> <motion.div animate={{ scale: 1.1 }}> Uses fast transition </motion.div> </MotionConfig> ); }
Best Practices
-
Use variants for complex multi-step animations instead of inline objects
-
Prefer spring physics over duration-based easing for natural motion
-
Only animate transform and opacity for 60fps performance
-
Always test with reduced motion enabled (System Preferences → Accessibility)
-
Use layoutId for shared element transitions between routes/states
-
Wrap exit animations in AnimatePresence with unique keys
-
Set willChange on elements with frequent animations
-
Use staggerChildren instead of manual delays for list animations
-
Combine layout + whileHover for dynamic interactive layouts
-
Keep transitions under 500ms for perceived performance
Anti-Patterns
-
❌ Animating width/height directly (use scale + layout instead)
-
❌ Forgetting AnimatePresence around conditional renders
-
❌ Hardcoding timing values (use constants)
-
❌ Ignoring prefers-reduced-motion
-
❌ Animating non-GPU properties (top, left, width, height, margin)
-
❌ Using motion on every element (overhead for static content)
-
❌ Deep nesting of layout animations (performance hit)
-
❌ Missing keys on AnimatePresence children
-
❌ Using exit without AnimatePresence
-
❌ Animating during SSR (causes hydration mismatches)
Feedback Loops
Animation quality:
Check frame rate in Chrome DevTools
Performance → Record → Look for dropped frames
Target: 60fps (16.67ms per frame)
Reduced motion test:
macOS: System Settings → Accessibility → Display → Reduce Motion
Test all animations with this enabled
Performance profiling:
// Use React DevTools Profiler // Measure commit duration with/without animations // Aim for <16ms commits