name framer-motion description Framer Motion animation patterns. Use when adding animations, transitions, gestures, or layout animations to React applications. Covers performance optimization and accessibility.
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' ;
<motion.div
initial ={{ opacity: 0 }}
animate ={{ opacity: 1 }}
transition ={{ duration: 0.3 }}
>
Content
</motion.div >
<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
const timing = {
fast : 0.15 ,
normal : 0.3 ,
slow : 0.5 ,
stagger : 0.05
};
<motion.div
animate ={{ x: 100 }}
transition ={{ duration: timing.normal , ease: 'easeInOut ' }}
/>
<motion.div
animate ={{ scale: 1.2 }}
transition ={{ type: 'spring ', stiffness: 300 , damping: 20 }}
/>
<motion.div
animate ={{ scale: [1 , 1.2 , 1 ] }}
transition ={{ duration: 0.5 , times: [0 , 0.5 , 1 ] }}
/>
<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 >
<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 >
<motion.div drag ="x" dragConstraints ={{ left: -100 , right: 100 }}>
Slide horizontal
</motion.div >
Layout Animations
Automatic Layout Animation
<motion.div layout>
{expanded ? <FullContent /> : <Summary /> }
</motion.div >
<motion.div layoutId ="card-123" >
<motion.img layoutId ="card-image-123" src ={image} />
</motion.div >
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' ;
<AnimatePresence mode ="wait" >
{isVisible && (
<motion.div
initial ={{ opacity: 0 }}
animate ={{ opacity: 1 }}
exit ={{ opacity: 0 }}
>
Content
</motion.div >
)}
</AnimatePresence >
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
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 >
import { useAnimate, stagger } from 'framer-motion' ;
const [scope, animate] = useAnimate ();
useEffect (() => {
animate ('.item' , { opacity : 1 }, { delay : stagger (0.05 ) });
}, []);
Performance
GPU-Accelerated Properties
<motion.div
animate={{
opacity : 1 ,
scale : 1.2 ,
x : 100 ,
rotate : 45
}}
/>
<motion.div
animate ={{
width: 300 , // Triggers layout
height: 200 , // Triggers layout
top: 50 // Triggers layout
}}
/>
willChange Optimization
<motion.div
style={{ willChange : 'transform' }}
whileHover={{ scale : 1.1 }}
>
Content
</motion.div >
<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 >
);
}
const prefersReducedMotion = useReducedMotion ();
<motion.div
{... (prefersReducedMotion ? {} : {
initial: { opacity: 0 },
animate: { opacity: 1 },
transition: { duration: 0.3 }
})}
>
Content
</motion.div >
Focus Management
<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' ;
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 }}
/>
);
}
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 >
);
}
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' ;
function App ( ) {
return (
<MotionConfig
reducedMotion ="user" // Respect prefers-reduced-motion
transition ={{ duration: 0.3 , ease: "easeOut " }}
>
<YourApp />
</MotionConfig >
);
}
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:
Reduced motion test:
Performance profiling: