Animating React with Framer Motion
A step-by-step guide to adding physics-based animations, layout transitions, and scroll effects to your React applications.
Tags
Animating React with Framer Motion
TL;DR
Framer Motion provides a declarative, spring-based animation API that makes adding performant animations to React components trivial, from simple mount transitions to complex drag-and-drop interfaces.
Prerequisites
- ›React 18+ with TypeScript
- ›Basic understanding of CSS transforms and transitions
- ›Node.js 18+ installed
- ›A React project (Next.js or Vite)
Step 1: Installation and Basic Motion Components
Install Framer Motion and start with the fundamental building block: the motion component.
npm install framer-motionEvery HTML element has a motion counterpart. Replace <div> with <motion.div> to unlock animation capabilities.
import { motion } from "framer-motion";
function FadeInCard() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="card"
>
<h2>Welcome</h2>
<p>This card fades in and slides up on mount.</p>
</motion.div>
);
}The three core props:
- ›
initial-- the starting state when the component mounts - ›
animate-- the target state to animate toward - ›
transition-- how the animation behaves (duration, easing, spring physics)
Spring vs Tween Animations
Framer Motion defaults to spring-based animations, which feel more natural than linear or eased tweens because they simulate real-world physics.
// Spring animation (default) - bouncy and natural
<motion.div
animate={{ x: 100 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
/>
// Tween animation - predictable duration
<motion.div
animate={{ x: 100 }}
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
/>Use springs for interactive elements (buttons, cards, modals) and tweens for sequences where you need precise timing control.
Step 2: Variants for Orchestrated Animations
Variants let you define named animation states and coordinate animations across parent and child components.
import { motion } from "framer-motion";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Each child animates 0.1s after the previous
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.li key={item} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}When the parent transitions to "visible", each child also transitions to its own "visible" state, but staggered by 0.1 seconds. This creates the cascade effect commonly seen in list animations.
Dynamic Variants
Variants can also be functions that accept custom arguments:
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: (index: number) => ({
opacity: 1,
x: 0,
transition: { delay: index * 0.05 },
}),
};
function AnimatedList({ items }: { items: string[] }) {
return (
<ul>
{items.map((item, index) => (
<motion.li
key={item}
custom={index}
variants={itemVariants}
initial="hidden"
animate="visible"
>
{item}
</motion.li>
))}
</ul>
);
}Step 3: AnimatePresence for Enter/Exit Animations
React removes elements from the DOM immediately when they unmount. AnimatePresence intercepts this and plays an exit animation first.
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
function Notification({ message, onClose }: { message: string; onClose: () => void }) {
return (
<motion.div
initial={{ opacity: 0, y: -50, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
className="notification"
>
<p>{message}</p>
<button onClick={onClose}>Dismiss</button>
</motion.div>
);
}
function NotificationStack() {
const [notifications, setNotifications] = useState<{ id: number; message: string }[]>([]);
const addNotification = () => {
const id = Date.now();
setNotifications((prev) => [...prev, { id, message: `Notification #${id}` }]);
};
const removeNotification = (id: number) => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
};
return (
<div>
<button onClick={addNotification}>Add Notification</button>
<AnimatePresence>
{notifications.map((n) => (
<Notification
key={n.id}
message={n.message}
onClose={() => removeNotification(n.id)}
/>
))}
</AnimatePresence>
</div>
);
}The key prop is critical. AnimatePresence uses it to track which elements are entering, present, or exiting. Always use unique, stable keys.
Mode Control
AnimatePresence accepts a mode prop to control how enter and exit animations interact:
// "sync" (default) - enter and exit happen simultaneously
<AnimatePresence mode="sync">
// "wait" - exit completes before enter starts
<AnimatePresence mode="wait">
// "popLayout" - exiting elements are removed from layout flow immediately
<AnimatePresence mode="popLayout">Use mode="wait" for page transitions where you want the old page to fully exit before the new page enters.
Step 4: Layout Animations
Layout animations automatically animate elements when their position or size changes in the DOM. Add the layout prop and Framer Motion handles the rest.
import { motion } from "framer-motion";
import { useState } from "react";
function ExpandableCard() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
style={{
width: isExpanded ? 400 : 200,
height: isExpanded ? 300 : 100,
borderRadius: 12,
background: "#3b82f6",
cursor: "pointer",
}}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
<motion.h3 layout="position">Click to {isExpanded ? "collapse" : "expand"}</motion.h3>
</motion.div>
);
}Shared Layout Animations
Use layoutId to animate an element between two different components or positions. Framer Motion automatically generates a smooth transition between them.
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
interface Tab {
id: string;
label: string;
}
const tabs: Tab[] = [
{ id: "home", label: "Home" },
{ id: "about", label: "About" },
{ id: "contact", label: "Contact" },
];
function AnimatedTabs() {
const [activeTab, setActiveTab] = useState(tabs[0].id);
return (
<div className="tab-bar">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className="tab-button"
>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="active-tab-indicator"
className="tab-indicator"
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>
)}
</button>
))}
</div>
);
}The indicator element slides smoothly between tabs because Framer Motion sees the same layoutId and animates from the old position to the new one.
Step 5: Scroll-Triggered Animations
Use useInView or whileInView to trigger animations when elements enter the viewport.
import { motion } from "framer-motion";
function ScrollRevealSection({ children }: { children: React.ReactNode }) {
return (
<motion.section
initial={{ opacity: 0, y: 60 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
{children}
</motion.section>
);
}- ›
viewport.once: truemeans the animation only plays once, not every time the element scrolls in and out - ›
viewport.marginshrinks the detection area so the animation triggers before the element fully enters the viewport
Scroll-Linked Animations
For animations that progress with the scroll position (parallax, progress bars), use useScroll:
import { motion, useScroll, useTransform } from "framer-motion";
function ParallaxHero() {
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
return (
<motion.div style={{ y, opacity }} className="hero-background">
<h1>Parallax Hero</h1>
</motion.div>
);
}
function ReadingProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
style={{ scaleX: scrollYProgress, transformOrigin: "left" }}
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 z-50"
/>
);
}useTransform maps one motion value to another. Here, as scrollYProgress goes from 0 to 1, the y position shifts from 0 to -200px, creating a parallax effect.
Step 6: Gesture Animations
Framer Motion provides built-in gesture detection for hover, tap, drag, and focus.
import { motion } from "framer-motion";
function InteractiveButton() {
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ boxShadow: "0 0 0 3px rgba(59, 130, 246, 0.5)" }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="px-6 py-3 bg-blue-500 text-white rounded-lg"
>
Click Me
</motion.button>
);
}Drag Gestures
Build draggable elements with constraints and snap-back behavior:
import { motion } from "framer-motion";
function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2}
dragTransition={{ bounceStiffness: 300, bounceDamping: 20 }}
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
className="w-32 h-32 bg-purple-500 rounded-xl cursor-grab"
/>
);
}- ›
dragConstraintslimits how far the element can be dragged - ›
dragElasticcontrols how much the element can be pulled beyond constraints (0 = rigid, 1 = fully elastic) - ›
dragTransitionconfigures the spring physics when the element snaps back
Putting It All Together
Here is a practical example combining several techniques -- an animated card grid with staggered reveals, hover effects, and a shared layout modal:
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
interface Project {
id: number;
title: string;
description: string;
}
const projects: Project[] = [
{ id: 1, title: "Dashboard", description: "Analytics dashboard with real-time charts" },
{ id: 2, title: "E-commerce", description: "Full-stack shopping platform" },
{ id: 3, title: "Chat App", description: "Real-time messaging with WebSocket" },
];
function ProjectGrid() {
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
return (
<>
<motion.div
className="grid grid-cols-3 gap-4"
initial="hidden"
animate="visible"
variants={{
hidden: {},
visible: { transition: { staggerChildren: 0.15 } },
}}
>
{projects.map((project) => (
<motion.div
key={project.id}
layoutId={`card-${project.id}`}
variants={{
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0 },
}}
whileHover={{ y: -4 }}
onClick={() => setSelectedProject(project)}
className="p-6 bg-white rounded-xl shadow cursor-pointer"
>
<motion.h3 layoutId={`title-${project.id}`}>{project.title}</motion.h3>
</motion.div>
))}
</motion.div>
<AnimatePresence>
{selectedProject && (
<motion.div
className="fixed inset-0 flex items-center justify-center z-50"
initial={{ backgroundColor: "rgba(0,0,0,0)" }}
animate={{ backgroundColor: "rgba(0,0,0,0.5)" }}
exit={{ backgroundColor: "rgba(0,0,0,0)" }}
onClick={() => setSelectedProject(null)}
>
<motion.div
layoutId={`card-${selectedProject.id}`}
className="bg-white rounded-xl p-8 max-w-lg w-full"
onClick={(e) => e.stopPropagation()}
>
<motion.h3 layoutId={`title-${selectedProject.id}`}>
{selectedProject.title}
</motion.h3>
<p>{selectedProject.description}</p>
<button onClick={() => setSelectedProject(null)}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
}The card smoothly expands from the grid into a centered modal using layoutId, and the title text animates seamlessly between positions.
Next Steps
- ›Page transitions -- wrap Next.js route changes with
AnimatePresencefor smooth page-to-page animations - ›SVG animations -- use
motion.pathwithpathLengthto animate line drawings - ›Accessibility -- respect
prefers-reduced-motionby wrapping animations withuseReducedMotion() - ›Performance profiling -- use React DevTools Profiler to ensure animations do not cause unnecessary re-renders
- ›Reusable animation hooks -- abstract common patterns (fade-in, slide-up, scale) into custom hooks
FAQ
What makes Framer Motion better than CSS animations for React?
Framer Motion provides physics-based spring animations, automatic layout transitions, gesture handling, and a declarative API that integrates naturally with React's component model, all without manual keyframe calculations.
Does Framer Motion impact performance?
Framer Motion is highly optimized, using hardware-accelerated transforms and the Web Animations API under the hood. It avoids triggering layout recalculations for most animations, keeping frame rates smooth.
How do you animate component mount and unmount in React?
Use Framer Motion's AnimatePresence component to wrap elements that may be conditionally rendered, combined with initial, animate, and exit props on motion components to define enter and exit animations.
Collaboration
Need help with a project?
Let's Build It
I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.
Related Articles
How to Add Observability to a Node.js App with OpenTelemetry
Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.
How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
A practical guide to building a Backend-for-Frontend with Next.js and Node.js for API aggregation, auth handling, caching, and frontend-specific data shaping.
How I Structure CI/CD for Next.js, Docker, and GitHub Actions
A practical CI/CD blueprint for Next.js apps using Docker and GitHub Actions, including testing, image builds, deployment stages, cache strategy, and release safety.