Mastering Framer Motion — Smooth Animations in React
A complete guide to Framer Motion — from your first animated div to variants, gestures, scroll effects, and layout animations.
Bagas
@bagas.coAnimation can be the difference between a good UI and an unforgettable one. Framer Motion is the go-to animation library for React — it gives you production-ready animations with remarkably little code. This guide walks you from zero to confident, covering every concept you'll actually use on the job.
Hit the Replay button on any example below to re-run the animation.
Installation
npm install framer-motionNo additional configuration needed — it works out of the box with Next.js, Vite, and Create React App.
1. The motion Component
Every HTML element has a motion equivalent. Swap <div> for <motion.div> and you unlock the full animation API.
import { motion } from "framer-motion";
// Before
<div className="box" />
// After — now animatable ✨
<motion.div className="box" />Available for every HTML tag: motion.p, motion.button, motion.img, motion.ul, motion.li, and so on.
2. initial, animate, and transition
These three props are the foundation of every Framer Motion animation.
| Prop | Purpose |
|---|---|
initial | The starting state |
animate | The target state to animate toward |
transition | Controls how the animation plays |
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
I fade in from below ✨
</motion.div>Faded in using easeOut with a subtle scale effect.
3. Transitions — Control the Feel
Framer Motion supports several transition types, each with a distinct character.
tween — Duration-based
transition={{ type: "tween", duration: 0.4, ease: "easeInOut" }}spring — Physics-based (most natural)
The most satisfying feel. Controlled by stiffness (how fast) and damping (how bouncy).
<motion.button
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 400, damping: 10 }}
>
Spring! 🌱
</motion.button>Stiffness & Damping Comparison
// Stiff spring — snappy
transition={{ type: "spring", stiffness: 600, damping: 30 }}
// Soft spring — floaty
transition={{ type: "spring", stiffness: 80, damping: 12 }}
// Bouncy spring — playful
transition={{ type: "spring", stiffness: 300, damping: 8 }}stiffness: 600 · damping: 30stiffness: 80 · damping: 12stiffness: 300 · damping: 84. Variants — Scalable Animation State
Once you animate more than two elements, hardcoding props everywhere becomes messy. Variants let you define named animation states and share them across a component tree.
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
};
function AnimatedList({ items }) {
return (
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.label}
</motion.li>
))}
</motion.ul>
);
}The magic: children inherit initial and animate from the parent automatically. The staggerChildren cascades through every child.
Navigation
- 🏠DashboardActive
- 📊Analytics
- 🗂️Projects
- ⚙️Settings
5. Gesture Animations
Framer Motion makes interactive animations trivial with dedicated gesture props.
whileHover and whileTap
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
Hover & click me
</motion.button>drag
<motion.div
drag="x"
dragConstraints={{ left: -120, right: 120 }}
whileDrag={{ scale: 1.1, rotate: 4 }}
>
Drag me ← →
</motion.div>Drag me!
6. AnimatePresence — Animate on Enter & Exit
React removes elements from the DOM instantly. AnimatePresence intercepts that removal and plays an exit animation first.
import { motion, AnimatePresence } from "framer-motion";
function Notification({ show, message }) {
return (
<AnimatePresence>
{show && (
<motion.div
key="notification"
initial={{ opacity: 0, y: -16, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -16, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
>
{message}
</motion.div>
)}
</AnimatePresence>
);
}Key rule: Always give the
motionelement insideAnimatePresencea stablekeyprop — it's how Framer Motion tracks which element to animate out.
7. Scroll Animations with useScroll and useTransform
Create scroll-driven progress bars, parallax effects, and elements that react to scroll position.
import { useScroll, useTransform, motion } from "framer-motion";
function ParallaxHero() {
const { scrollY } = useScroll();
// opacity goes 1 → 0 as user scrolls 0 → 300px
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
// background moves at 40% scroll speed (parallax)
const y = useTransform(scrollY, [0, 300], [0, 120]);
return (
<motion.section style={{ opacity, y }}>
<h1>Scroll to fade me</h1>
</motion.section>
);
}Reading Progress Bar
function ReadingProgressBar() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="fixed top-0 left-0 h-1 bg-primary origin-left"
style={{ scaleX: scrollYProgress }}
/>
);
}8. Layout Animations
One of Framer Motion's most impressive features — animate layout changes automatically.
// Reordering items? just add `layout`
<motion.div layout>
{item.name}
</motion.div>// Shared element transitions between states
<motion.img
layoutId={`product-image-${product.id}`}
src={product.image}
/>9. Real-World Patterns
Page Transition Wrapper
export function PageTransition({ children }) {
return (
<motion.main
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.25, ease: "easeInOut" }}
>
{children}
</motion.main>
);
}Staggered Card Grid
const gridVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.08 } },
};
const cardVariants = {
hidden: { opacity: 0, scale: 0.92, y: 16 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: "spring", stiffness: 260, damping: 20 },
},
};
function ProjectGrid({ projects }) {
return (
<motion.div
className="grid grid-cols-3 gap-6"
variants={gridVariants}
initial="hidden"
animate="visible"
>
{projects.map((project) => (
<motion.div key={project.id} variants={cardVariants}>
<h3>{project.name}</h3>
</motion.div>
))}
</motion.div>
);
}Animated Accordion Item
function AccordionItem({ title, content }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<motion.button
onClick={() => setIsOpen(!isOpen)}
whileTap={{ scale: 0.98 }}
>
{title}
<motion.span
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
>
▼
</motion.span>
</motion.button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
key="content"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: "easeInOut" }}
style={{ overflow: "hidden" }}
>
{content}
</motion.div>
)}
</AnimatePresence>
</div>
);
}10. Performance Tips
Animate only GPU-accelerated properties. Stick to transform and opacity — these never trigger layout recalculations.
// ✅ GPU-accelerated — smooth at 60fps
animate={{ x: 100, y: 50, scale: 1.1, rotate: 5, opacity: 0.8 }}
// ❌ Triggers layout paint — avoid
animate={{ width: 200, height: 100, top: 50, marginLeft: 20 }}Respect user accessibility preferences:
import { useReducedMotion } from "framer-motion";
function HeroCard() {
const shouldReduce = useReducedMotion();
return (
<motion.div
initial={shouldReduce ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={shouldReduce ? { duration: 0 } : { type: "spring" }}
/>
);
}Quick Reference
import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion";
// All core props at a glance
<motion.div
// Entry & exit states
initial={{ opacity: 0, scale: 0.9, x: -20 }}
animate={{ opacity: 1, scale: 1, x: 0 }}
exit={{ opacity: 0, scale: 0.9 }}
// Transition
transition={{ type: "spring", stiffness: 260, damping: 20, delay: 0.1 }}
// Gestures
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.96 }}
drag="x"
dragConstraints={{ left: -100, right: 100 }}
// Variants
variants={myVariants}
// Layout
layout
layoutId="unique-shared-id"
/>Framer Motion rewards you quickly — a few lines produce animations that would take hundreds of lines of raw CSS. Start with initial and animate, reach for variants when logic repeats, then let AnimatePresence and useScroll handle the polish.
Happy animating. 🎬