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)
import { motion, AnimatePresence } from 'motion/react'
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>
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>
)
}
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>
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>
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 }}
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>
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>
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>
)
}
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>
)
}
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>
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 }}
/>
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 }} />
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 }} />
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
Top comments (2)
I'm curious about the impact of the package size reduction on performance, do you have any benchmarks? I'd love to swap ideas on optimizing motion animations.
Good question — the bundle impact is actually one of the most underrated reasons to migrate from framer-motion to motion.
The full framer-motion package is around 106 KB minified (roughly 34 KB gzipped). The newer motion package improves on that through better tree-shaking, but the biggest optimization comes from LazyMotion, which many migration guides barely mention.
LazyMotion + m component is the pattern that really matters:
import { LazyMotion, domAnimation, m } from "motion/react";
// Wrap your app or section
{/* Use instead of */}
domAnimation is about 17 KB gzipped and covers roughly 95% of real-world use cases: basic animations, exit animations, and gestures. domMax adds heavier features such as layout animations and drag support, bringing the size to around 34 KB gzipped.
If you only need those advanced features on certain routes, you can lazy-load them:
const loadFeatures = () =>
import("motion/react").then((m) => m.domMax);
...
In practice, switching from to LazyMotion + with domAnimation in a Next.js application can reduce the motion-related JavaScript from roughly 34 KB to 17 KB gzipped—about a 50% reduction for that part of the main bundle.
For published benchmarks, Matt Perry (the library's author) shared size comparisons when motion was introduced. The main takeaway is that selective feature loading with LazyMotion typically reduces the animation bundle by around 40–60%, depending on which features your application actually uses.
To measure the impact in your own project, the most reliable approach is to compare bundle analyses before and after the migration using next build --analyze (or a tool such as Bundle Buddy), since the exact savings depend on which Motion APIs you're using.
Out of curiosity, are your animations mostly simple entrance/fade effects, or do you rely on heavier features like layoutId, layout transitions, or drag interactions?