Framer Motion / Motion: React Animations Complete Guide (2026)

The library formerly known as Framer Motion is now just motion. Same team, same API, leaner package. The patterns that were experimental in v10 are stable, and scroll-driven animations are now the standard. Most animation tutorials show a spinning box. This guide shows what you actually reach for: exit animations, shared layout transitions, scroll effects, stagger lists, gesture interactions. npm install motion # or: npm install framer-motion (still works, re-exports from motion) import { motion
The library formerly known as Framer Motion is now just motion. Same team, same API, leaner package. The patterns that were experimental in v10 are stable, and scroll-driven animations are now the standard.
Most animation tutorials show a spinning box. This guide shows what you actually reach for: exit animations, shared layout transitions, scroll effects, stagger lists, gesture interactions.
Installation
npm install motion
# or: npm install framer-motion (still works, re-exports from motion)
Enter fullscreen mode Exit fullscreen mode
import { motion, AnimatePresence } from 'motion/react'
Enter fullscreen mode Exit fullscreen mode
The motion Component
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
>
Content
</motion.div>
Enter fullscreen mode Exit fullscreen mode
Stagger with Variants
const container = {
hidden: {},
visible: { transition: { staggerChildren: 0.08 } },
}
const item = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.35 } },
}
function AnimatedList({ items }: { items: string[] }) {
return (
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map(i => <motion.li key={i} variants={item}>{i}</motion.li>)}
</motion.ul>
)
}
Enter fullscreen mode Exit fullscreen mode
Children animate 80ms apart, automatically.
AnimatePresence: Exit Animations
React unmounts instantly — AnimatePresence lets the exit prop play before removal:
<AnimatePresence>
{toasts.map(toast => (
<motion.div
key={toast.id}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
{toast.message}
</motion.div>
))}
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode
For switching between two components, use mode="wait":
<AnimatePresence mode="wait">
<motion.div key={currentTab} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
{currentTab === 'a' ? <TabA /> : <TabB />}
</motion.div>
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode
Spring Physics
// Snappy, no bounce
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
// Bouncy
transition={{ type: 'spring', stiffness: 200, damping: 10 }}
// Slow, heavy
transition={{ type: 'spring', stiffness: 80, damping: 20 }}
Enter fullscreen mode Exit fullscreen mode
Layout Animations
The layout prop animates position/size changes when the component moves in the DOM:
<AnimatePresence>
{filtered.map(item => (
<motion.li
key={item.id}
layout // ← smoothly repositions when filter changes
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
>
<ItemCard item={item} />
</motion.li>
))}
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode
layoutId: Shared Layout Morphing
The most impressive feature — elements with the same layoutId morph between each other's positions:
// In the grid
<motion.div layoutId={`card-${image.id}`} onClick={() => setSelected(image)}>
<motion.img layoutId={`img-${image.id}`} src={image.src} />
</motion.div>
// In the modal (completely different position in the tree)
<motion.div layoutId={`card-${image.id}`} className="fixed inset-4">
<motion.img layoutId={`img-${image.id}`} src={image.src} />
</motion.div>
Enter fullscreen mode Exit fullscreen mode
The card morphs from grid to modal. Zero position calculation needed.
Scroll-Driven Animations
import { useScroll, useTransform } from 'motion/react'
function ParallaxSection() {
const ref = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start start', 'end start'],
})
const y = useTransform(scrollYProgress, [0, 1], ['0%', '30%'])
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
return (
<div ref={ref} className="relative h-[80vh] overflow-hidden">
<motion.div style={{ y, opacity }} className="absolute inset-0 bg-gradient-to-b from-blue-900 to-purple-900" />
</div>
)
}
Enter fullscreen mode Exit fullscreen mode
Animate on Scroll Into View
import { useInView } from 'motion/react'
function AnimateOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-100px 0px' })
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
{children}
</motion.div>
)
}
Enter fullscreen mode Exit fullscreen mode
Gesture Animations
<motion.div
whileHover={{ scale: 1.02, y: -4 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="cursor-pointer rounded-xl border p-6"
>
Card content
</motion.div>
Enter fullscreen mode Exit fullscreen mode
SVG Path Animation
<motion.path
d="M14 27 L21 34 L38 17"
fill="none" stroke="currentColor" strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4, delay: 0.5 }}
/>
Enter fullscreen mode Exit fullscreen mode
Performance
// ✅ GPU-accelerated (fast)
animate={{ x: 100, scale: 1.1, opacity: 0.8 }}
// ❌ Triggers layout recalculation (slow)
animate={{ width: 200, height: 100 }}
// Respect prefers-reduced-motion
const shouldReduce = useReducedMotion()
<motion.div initial={{ y: shouldReduce ? 0 : 40 }} animate={{ y: 0 }} />
Enter fullscreen mode Exit fullscreen mode
Quick Reference
// Basic
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
// Spring
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
// Stagger children
const container = { visible: { transition: { staggerChildren: 0.08 } } }
// Exit animations
<AnimatePresence mode="wait"><motion.div key={id} exit={{ opacity: 0 }} /></AnimatePresence>
// Shared layout
<motion.div layoutId="hero-image" /> // same ID in grid and modal
// Scroll-driven
const { scrollYProgress } = useScroll({ target: ref })
const opacity = useTransform(scrollYProgress, [0, 1], [1, 0])
// Gestures
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} />
Enter fullscreen mode Exit fullscreen mode
The patterns with the biggest impact: AnimatePresence for conditional rendering, layoutId for elements that morph between states, and staggerChildren for list entrances.
Full article at stacknotice.com/blog/framer-motion-react-animations-2026



