Loading…
Loading…
A step-by-step guide to adding physics-based animations, layout transitions, and scroll effects to your React applications.
Tags
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.
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 mountsanimate -- the target state to animate towardtransition -- how the animation behaves (duration, easing, spring physics)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.
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.
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>
);
}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.
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.
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>
);
}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.
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: true means the animation only plays once, not every time the element scrolls in and outviewport.margin shrinks the detection area so the animation triggers before the element fully enters the viewportFor 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.
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>
);
}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"
/>
);
}dragConstraints limits how far the element can be draggeddragElastic controls how much the element can be pulled beyond constraints (0 = rigid, 1 = fully elastic)dragTransition configures the spring physics when the element snaps backHere 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.
AnimatePresence for smooth page-to-page animationsmotion.path with pathLength to animate line drawingsprefers-reduced-motion by wrapping animations with useReducedMotion()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.
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.
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
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.
Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.
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.
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.