React Framer Motion: Animations and Transitions Guide (2026)
Framer Motion is the production animation library for React. It handles enter/exit animations, layout transitions, drag gestures, scroll-driven effects and spring physics — all with a declarative API that stays in harmony with React's rendering model. This guide covers everything from basic motion components to advanced orchestration with variants.
Table of Contents
Motion Components and Basic Animations
npm install framer-motion
import { motion } from 'framer-motion'
// motion.div = regular div + animation superpowers
function FadeIn() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }} // Start state
animate={{ opacity: 1, y: 0 }} // End state
transition={{ duration: 0.4, ease: 'easeOut' }}
>
Hello, animated world!
</motion.div>
)
}
// Keyframes — array of values
<motion.div
animate={{ x: [0, 100, 0] }}
transition={{ duration: 2, repeat: Infinity }}
/>
// Animate on state change
function Toggle() {
const [open, setOpen] = useState(false)
return (
<motion.div
animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }}
transition={{ duration: 0.3 }}
style={{ overflow: 'hidden' }}
>
Content
</motion.div>
)
}
Variants and Orchestration
Variants let you define named animation states and propagate them to children — essential for staggered list animations:
import { motion } from 'framer-motion'
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Each child animates 100ms after previous
delayChildren: 0.2, // Wait 200ms before starting children
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { type: 'spring', stiffness: 300, damping: 24 },
},
}
function AnimatedList({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.title}
</motion.li>
))}
</motion.ul>
)
}
// Children inherit parent's initial/animate — no need to repeat them
AnimatePresence: Enter/Exit
AnimatePresence enables exit animations — components play their exit animation before unmounting:
import { AnimatePresence, motion } from 'framer-motion'
// Modal with enter/exit
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
key="backdrop"
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* Modal panel */}
<motion.div
key="modal"
className="fixed inset-x-4 top-1/2 -translate-y-1/2 bg-white rounded-xl p-6"
initial={{ opacity: 0, scale: 0.9, y: '-48%' }}
animate={{ opacity: 1, scale: 1, y: '-50%' }}
exit={{ opacity: 0, scale: 0.9, y: '-48%' }}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
// Page transitions with React Router
function PageWrapper({ children }) {
const location = useLocation()
return (
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
</AnimatePresence>
)
}
Layout Animations
The layout prop automatically animates when a component's size or position changes — no keyframes needed:
// Animates position/size changes automatically
function ExpandableCard({ title, content }) {
const [expanded, setExpanded] = useState(false)
return (
<motion.div
layout // Animates layout changes
onClick={() => setExpanded(!expanded)}
className="card cursor-pointer"
style={{ borderRadius: 16 }} // Must be inline for layout anim
>
<motion.h3 layout="position">{title}</motion.h3>
{expanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{content}
</motion.p>
)}
</motion.div>
)
}
// Shared layout animation — element "moves" between positions
// Wrap both with LayoutGroup and give them matching layoutId
import { LayoutGroup } from 'framer-motion'
function Tabs({ tabs }) {
const [active, setActive] = useState(tabs[0].id)
return (
<LayoutGroup>
<div className="flex gap-2">
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActive(tab.id)} className="relative px-4 py-2">
{tab.label}
{active === tab.id && (
<motion.div
layoutId="active-indicator" // Same ID = shared animation
className="absolute inset-0 bg-indigo-500 rounded-md"
style={{ zIndex: -1 }}
/>
)}
</button>
))}
</div>
</LayoutGroup>
)
}
Gestures: Hover, Tap, Drag
// Hover and tap
<motion.button
whileHover={{ scale: 1.05, boxShadow: '0 8px 32px rgba(99,102,241,0.3)' }}
whileTap={{ scale: 0.97 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
Click me
</motion.button>
// Draggable card with constraints
function DraggableCard() {
const constraintsRef = useRef(null)
return (
<div ref={constraintsRef} className="relative h-64 w-full border rounded-xl overflow-hidden">
<motion.div
drag
dragConstraints={constraintsRef}
dragElastic={0.2}
whileDrag={{ scale: 1.05, cursor: 'grabbing' }}
className="absolute w-24 h-24 bg-indigo-500 rounded-xl cursor-grab flex items-center justify-center text-white"
>
Drag me
</motion.div>
</div>
)
}
// Drag to reorder — combined with useDragControls
import { useDragControls } from 'framer-motion'
function SortableItem({ item }) {
const controls = useDragControls()
return (
<motion.div drag="y" dragControls={controls} dragListener={false}>
<span onPointerDown={(e) => controls.start(e)}>⠿</span>
{item.title}
</motion.div>
)
}
Scroll-Driven Animations
import { motion, useScroll, useTransform, useInView } from 'framer-motion'
// Parallax effect
function ParallaxHero() {
const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 500], [0, -150]) // Map scroll to y offset
return (
<div className="relative h-screen overflow-hidden">
<motion.img src="/hero.jpg" alt="Hero" style={{ y }} className="absolute inset-0 w-full h-[120%] object-cover" />
</div>
)
}
// Animate when element scrolls into view
function AnimateOnScroll({ children }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-100px' })
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 40 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
)
}
// Progress bar tied to scroll position
function ReadingProgress() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-indigo-500 origin-left z-50"
style={{ scaleX: scrollYProgress }}
/>
)
}
Spring Physics
// Spring types
transition={{ type: 'spring', stiffness: 300, damping: 30 }} // Bouncy
transition={{ type: 'spring', stiffness: 100, damping: 20 }} // Slow bounce
transition={{ type: 'spring', stiffness: 400, damping: 50 }} // Snappy, no bounce
// useSpring for value-driven animations
import { useSpring, motion } from 'framer-motion'
function SpringCounter({ value }) {
const spring = useSpring(value, { stiffness: 100, damping: 30 })
return <motion.span>{spring}</motion.span>
}
Performance Best Practices
- Animate only transform and opacity — these run on the GPU compositor thread without triggering layout/paint. Avoid animating
width,height,top,left - Use
will-change: transformon elements that animate frequently — but sparingly (each creates a compositor layer) - Prefer
layoutprop over animating dimensions — Framer Motion uses FLIP technique which is GPU-accelerated - Set
once: trueonuseInView— stops observing after first trigger, avoids scroll-event overhead - Use
LazyMotionto tree-shake unused features in production bundles
import { LazyMotion, domAnimation, m } from 'framer-motion'
// Load only DOM animations (not 3D) — smaller bundle
function App() {
return (
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }}>Uses smaller bundle</m.div>
</LazyMotion>
)
}