What is Framer Motion? Framer Motion is a declarative animation library for React that replaces CSS keyframes with JavaScript objects. It provides the motion component wrapper, AnimatePresence for exit animations, and automatic layout animations. Complex sequences that would take 100 lines of CSS become 10 lines of code.
I’ve shipped Framer Motion in production apps handling millions of users. This guide distills those lessons: what actually works, what breaks under pressure, and the patterns worth adopting. Skip the theory. This is the practical playbook.
TL;DR: Replace
<div>with<motion.div>, define animations withinitial/animate/exitprops, wrap conditionally rendered elements inAnimatePresencewith a uniquekey, and stick totransform/opacityfor 60fps performance. UseLazyMotionwithdomAnimationto cut bundle size from 30kb to 15kb.
Last tested with Framer Motion 12.25.0 and Next.js 16.1.1 in January 2026
Getting Started
Replace <div> with <motion.div>, add animation props, done. That’s the core mental model.
This guide covers Framer Motion v10+ (also known as “Motion”). Most patterns work in v6+, but useAnimate, useInView, and some LazyMotion features require v10. Check your version with npm list framer-motion.
Installation
npm install framer-motion
# or
yarn add framer-motion
# or
pnpm add framer-motion
For Next.js App Router projects, remember that motion components are client-side only. Add the "use client" directive at the top of any file using Framer Motion:
"use client";
import { motion } from "framer-motion";
The Mental Model
Replace standard HTML elements with their motion equivalents. A <div> becomes <motion.div>, a <button> becomes <motion.button>. These components accept animation props:
import { motion } from "framer-motion";
function FadeInBox() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="p-6 bg-blue-500 rounded-lg"
>
I fade in on mount
</motion.div>
);
}
The three core props are:
initial: The starting state (can befalseto disable mount animations)animate: The target state to animate towardtransition: How the animation should behave (duration, easing, spring physics)
Copy/Paste Starter Kit
Basic Motion Component:
import { motion } from "framer-motion";
export function SlideIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
Variants Example:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
export 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>
);
}
AnimatePresence Example:
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
export function ToggleContent() {
const [isVisible, setIsVisible] = useState(false);
return (
<>
<button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
<AnimatePresence>
{isVisible && (
<motion.div
key="content"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
>
Content that animates in and out
</motion.div>
)}
</AnimatePresence>
</>
);
}
Reduced Motion Example:
import { motion, useReducedMotion } from "framer-motion";
export function AccessibleSlideIn({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, x: shouldReduceMotion ? 0 : -100 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: shouldReduceMotion ? 0.1 : 0.4 }}
>
{children}
</motion.div>
);
}
Do/Don’t List
| Do | Don’t |
|---|---|
Use motion.div for animatable elements | Expect animate/initial props to work on regular div |
Add "use client" in Next.js App Router | Forget the directive and get cryptic errors |
| Start with simple opacity/transform animations | Jump straight to complex layout animations |
| Test animations on lower-powered devices | Only test on your development machine |
Field Notes: Getting Started
- What works: Simple fade and slide animations are nearly bulletproof. Start here before adding complexity.
- What breaks: Forgetting
"use client"in Next.js causes acreateContexterror that doesn’t immediately point to Framer Motion. - How to fix it: If you see context-related errors, check that all files importing from
framer-motionhave the client directive. - Practical recommendation: Create a
components/motionfolder with pre-configured motion components that include your app’s default transitions.
Framer Motion vs Alternatives
How does Framer Motion compare to other animation libraries? Framer Motion offers the best React integration and automatic layout animations, but comes with a larger bundle than CSS-only solutions. Here’s how it stacks up:
| Feature | Framer Motion | CSS Animations | React Spring | GSAP |
|---|---|---|---|---|
| Bundle Size | ~30kb (full) / ~15kb (lazy) | 0kb | ~25kb | ~60kb |
| Exit Animations | ✅ AnimatePresence | ❌ Manual JS required | ⚠️ useTransition (complex) | ✅ Manual timeline |
| Layout Animations | ✅ Automatic FLIP | ❌ Not supported | ❌ Not supported | ⚠️ Manual FLIP |
| Gesture Support | ✅ Built-in drag, hover, tap | ❌ JS required | ❌ Separate library | ✅ Draggable plugin |
| Learning Curve | Medium | Low | High (physics model) | High (timeline API) |
| React Integration | Native (declarative) | CSS-in-JS needed | Native | Wrapper/refs needed |
| SSR Support | ✅ With hydration handling | ✅ Native | ✅ Native | ⚠️ Complex setup |
| TypeScript | ✅ First-class | N/A | ✅ First-class | ⚠️ @types package |
When to choose Framer Motion: You need exit animations, layout animations, or gesture support without writing imperative code. The declarative API integrates naturally with React’s component model.
When to choose alternatives:
- CSS Animations: Simple hover states, loading spinners. No library needed
- React Spring: Physics-based animations where you need precise spring control
- GSAP: Complex timelines, SVG morphing, or ScrollTrigger-heavy pages
When to Use Each Animation Approach
Framer Motion offers multiple ways to animate. Here’s when to use each:
Use Inline animate Props When:
- The animation is simple (fade, slide, scale)
- It’s a one-off animation not reused elsewhere
- You’re animating based on a single boolean state
// Simple fade-in on mount
<motion.div animate={{ opacity: 1 }} initial={{ opacity: 0 }} />
Use Variants When:
- Parent and child components need coordinated animations
- You want staggered list animations with
staggerChildren - The same animation states are reused across components
- You need named states like “hidden”, “visible”, “exit”
// Coordinated parent-child animations
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
<motion.li variants={itemVariants} />
</motion.ul>
Use useAnimate When:
- Animations depend on async operations (API calls, clipboard copy)
- You need precise sequencing control beyond variants
- The animation is imperative. It fires once and doesn’t persist in state
// Sequence after async operation
const [scope, animate] = useAnimate();
await copyToClipboard();
await animate(scope.current, { scale: 1.1 });
Use Layout Animations When:
- Element size or position changes based on content/state
- You’re building reorderable lists or grids
- You need “shared element” transitions between views with
layoutId
// Automatic size/position animation
<motion.div layout style={{ width: isExpanded ? 400 : 200 }} />
Use useMotionValue + useTransform When:
- Animations should respond to continuous input (scroll, drag position)
- You need to derive one animated value from another
- Performance is critical. This approach avoids React re-renders
// Scroll-linked parallax
const { scrollY } = useScroll();
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
Core Concepts (variants, gestures)
What are Framer Motion variants? Variants are named animation states that coordinate animations across parent and child components. Instead of passing animation objects directly, you reference variant names as strings, enabling orchestrated sequences with staggerChildren and delayChildren.
Variants: Orchestrated Animations
Variants let you define named animation states and coordinate animations across parent and child components. Instead of passing animation objects directly, you reference variant names as strings.
import { motion, Variants } from "framer-motion";
const cardVariants: Variants = {
offscreen: {
y: 50,
opacity: 0,
},
onscreen: {
y: 0,
opacity: 1,
transition: {
type: "spring",
bounce: 0.4,
duration: 0.8,
},
},
};
export function AnimatedCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
variants={cardVariants}
initial="offscreen"
whileInView="onscreen"
viewport={{ once: true, amount: 0.3 }}
className="bg-white rounded-xl shadow-lg p-6"
>
{children}
</motion.div>
);
}
The Variants type from Framer Motion provides full TypeScript support for variant objects. For more on React patterns, see the React Hooks guide.
Parent-Child Orchestration
Variants flow down through nested motion components. Children automatically inherit variant names from parents, enabling staggered animations without explicit coordination:
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
delayChildren: 0.3,
staggerChildren: 0.1,
// Pro tip: use staggerDirection: -1 to reverse the stagger order
},
},
};
const itemVariants: Variants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: "spring", stiffness: 100 },
},
};
export function NavigationMenu({ items }: { items: NavItem[] }) {
return (
<motion.nav
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.a
key={item.href}
href={item.href}
variants={itemVariants}
className="block py-2 px-4 hover:bg-gray-100"
>
{item.label}
</motion.a>
))}
</motion.nav>
);
}
Notice that child motion.a elements don’t need initial or animate props. They inherit the variant names from the parent and use their own itemVariants to define what those states mean.
Gestures: Interactive Animations
Framer Motion provides gesture props that make interactive animations trivial:
export function InteractiveButton({ children }: { children: React.ReactNode }) {
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="px-6 py-3 bg-indigo-600 text-white rounded-lg"
>
{children}
</motion.button>
);
}
Drag Gestures
The drag prop enables draggable elements with physics-based behavior:
export function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2}
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
className="w-32 h-32 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl cursor-grab"
/>
);
}
For constrained dragging within a parent container:
import { motion } from "framer-motion";
import { useRef } from "react";
export function DraggableWithinBounds() {
const constraintsRef = useRef<HTMLDivElement>(null);
return (
<div ref={constraintsRef} className="w-full h-64 bg-gray-100 rounded-lg">
<motion.div
drag
dragConstraints={constraintsRef}
dragMomentum={false}
className="w-16 h-16 bg-blue-500 rounded-lg"
/>
</div>
);
}
Pointer-Friendly UX Patterns
When building touch-friendly interfaces, combine gestures thoughtfully:
export function SwipeableCard({ onDismiss }: { onDismiss: () => void }) {
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 100) {
onDismiss();
}
}}
whileTap={{ cursor: "grabbing" }}
className="bg-white rounded-xl shadow-lg p-4"
>
Swipe to dismiss
</motion.div>
);
}
Do/Don’t List
| Do | Don’t |
|---|---|
| Use variants for coordinated parent-child animations | Define animations inline when they’re reused |
Keep staggerChildren values small (0.05-0.15s) | Use large stagger values that feel sluggish |
Add dragConstraints to prevent elements flying off-screen | Enable drag without boundaries |
Use whileTap for touch feedback on mobile | Rely only on whileHover which doesn’t work on touch |
Field Notes: Variants and Gestures
- What works: Variants with
staggerChildrencreate professional-feeling list animations with minimal code. The parent-child inheritance is intuitive once you understand it. - What breaks: Adding an
animateprop to a child component breaks variant inheritance. The child will ignore the parent’s variant state. - How to fix it: Remove explicit
animateprops from children that should inherit from parents. Only definevariantson children. - Practical recommendation: Create a variants library file (
lib/motion-variants.ts) with your app’s standard animation patterns. This ensures consistency and makes animations easy to audit. If you’re building a component library, see the design systems guide for organizing reusable motion patterns.
Scroll Animations
How do I create scroll-linked animations in Framer Motion? Use the useScroll hook to get MotionValue objects representing scroll progress (0 to 1), then useTransform to map those values to animation properties. This approach uses requestAnimationFrame under the hood and avoids React re-renders for smooth 60fps performance.
The useScroll Hook
The useScroll hook returns MotionValue objects representing scroll progress. Under the hood, it uses Intersection Observer for element tracking and requestAnimationFrame for smooth updates without triggering React re-renders:
import { motion, useScroll } from "framer-motion";
export function ScrollProgressBar() {
// Track page scroll progress (0 to 1)
const { scrollYProgress } = useScroll();
return (
<motion.div
style={{ scaleX: scrollYProgress }}
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left z-50"
/>
);
}
For tracking scroll within a specific container:
import { motion, useScroll } from "framer-motion";
import { useRef } from "react";
export function ContainerScrollProgress() {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
container: containerRef,
});
return (
<div ref={containerRef} className="h-96 overflow-y-auto">
<motion.div style={{ opacity: scrollYProgress }}>
Content that fades in as you scroll
</motion.div>
</div>
);
}
Mapping Scroll to Animations with useTransform
The useTransform hook maps one range of values to another. Perfect for creating parallax effects:
import { motion, useScroll, useTransform } from "framer-motion";
export function ParallaxHero() {
const { scrollY } = useScroll();
// Map scroll position to y offset (slower than scroll = parallax)
const y = useTransform(scrollY, [0, 500], [0, 150]);
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
return (
<motion.div
style={{ y, opacity }}
className="h-screen flex items-center justify-center bg-gradient-to-b from-purple-600 to-blue-600"
>
<h1 className="text-6xl font-bold text-white">
Parallax Hero
</h1>
</motion.div>
);
}
Scroll-Triggered Element Animations
Track when a specific element enters the viewport using the target option:
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef } from "react";
export function RevealOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"], // When element enters/leaves viewport
});
const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]);
const scale = useTransform(scrollYProgress, [0, 0.3], [0.8, 1]);
return (
<motion.div ref={ref} style={{ opacity, scale }}>
{children}
</motion.div>
);
}
For simpler visibility detection without scroll-linked values, use the useInView hook:
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
export function FadeInWhenVisible({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}
Do/Don’t List
| Do | Don’t |
|---|---|
Use useTransform for smooth value mapping | Manually calculate positions in useEffect |
Apply will-change: transform sparingly | Add will-change to many elements |
| Clean up scroll listeners (hooks do this automatically) | Create manual scroll listeners without cleanup |
Combine with whileInView for simpler reveal effects | Overcomplicate with useScroll when whileInView suffices |
Field Notes: Scroll Animations
- What works: Parallax and progress indicators are nearly plug-and-play with
useScrollanduseTransform. The hooks handle cleanup automatically. - What breaks: Performance can degrade if you animate expensive properties (like blur) based on scroll. Mobile browsers may throttle scroll events.
- How to fix it: Stick to transform and opacity. Use
useSpringto smooth out jerky scroll values if needed. - Practical recommendation: For simple “fade/slide in when visible” effects, prefer
whileInVieworuseInViewover scroll hooks. They’re simpler and more performant.
Layout Animations
How do Framer Motion layout animations work? The layout prop uses the FLIP technique (First, Last, Invert, Play) to animate layout changes using GPU-accelerated transform instead of expensive properties like width and height. The browser measures positions before and after state changes, then Framer Motion applies an inverse transform and animates it back to zero. This keeps animations on the compositor thread.

import { motion } from "framer-motion";
import { useState } from "react";
export function ExpandableCard() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
className={`bg-white rounded-xl shadow-lg cursor-pointer ${
isExpanded ? "p-8" : "p-4"
}`}
style={{ width: isExpanded ? 400 : 200 }}
>
<motion.h3 layout="position" className="font-bold">
{/* layout="position" prevents text from scaling/squishing during the box resize */}
Click to expand
</motion.h3>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-4 text-gray-600"
>
Additional content appears here with a smooth animation.
</motion.p>
)}
</motion.div>
);
}
List Reordering
Layout animations enable smooth list reordering with the Reorder component:
import { motion, Reorder } from "framer-motion";
import { useState } from "react";
interface Task {
id: string;
title: string;
}
export function ReorderableTaskList() {
const [tasks, setTasks] = useState<Task[]>([
{ id: "1", title: "Review pull requests" },
{ id: "2", title: "Update documentation" },
{ id: "3", title: "Fix accessibility issues" },
]);
return (
<Reorder.Group
axis="y"
values={tasks}
onReorder={setTasks}
className="space-y-2"
>
{tasks.map((task) => (
<Reorder.Item
key={task.id}
value={task}
className="bg-white p-4 rounded-lg shadow cursor-grab active:cursor-grabbing"
>
{task.title}
</Reorder.Item>
))}
</Reorder.Group>
);
}
Shared Layout Animations with layoutId
The layoutId prop creates “magic motion” effects where two separate elements animate between each other:
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
interface Item {
id: string;
title: string;
description: string;
}
export function ExpandableGrid({ items }: { items: Item[] }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedItem = items.find((item) => item.id === selectedId);
return (
<>
<div className="grid grid-cols-3 gap-4">
{items.map((item) => (
<motion.div
key={item.id}
layoutId={item.id}
onClick={() => setSelectedId(item.id)}
className="bg-white p-4 rounded-xl shadow-lg cursor-pointer"
>
<motion.h3 layoutId={`title-${item.id}`}>{item.title}</motion.h3>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && selectedItem && (
<motion.div
layoutId={selectedId}
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={() => setSelectedId(null)}
>
<motion.div className="bg-white p-8 rounded-xl shadow-2xl max-w-lg">
<motion.h3 layoutId={`title-${selectedId}`} className="text-2xl font-bold">
{selectedItem.title}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="mt-4"
>
{selectedItem.description}
</motion.p>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
}
Layout Animation Options
The layout prop accepts different values for specific behaviors:
layout={true}- Animate both position and sizelayout="position"- Only animate position (useful for images/text that shouldn’t scale)layout="size"- Only animate size changes
// For images that shouldn't distort during layout changes
<motion.img
layout="position"
src="/avatar.jpg"
className="w-16 h-16 rounded-full"
/>
Advanced: Scrollable Containers and Fixed Elements
When animating within scrollable containers, add layoutScroll to account for scroll offset:
<motion.div layoutScroll className="overflow-y-auto h-96">
{items.map((item) => (
<motion.div key={item.id} layout>
{item.content}
</motion.div>
))}
</motion.div>
For fixed-position containers, use layoutRoot:
<motion.div layoutRoot className="fixed top-0 left-0">
<motion.div layout>
Content that animates correctly within fixed container
</motion.div>
</motion.div>
Anti-Pattern: Accidental Layout Triggers
A common mistake is animating CSS properties that trigger browser layout recalculations, defeating the performance benefits:
// ❌ Bad: Animating width triggers layout every frame
<motion.div
animate={{ width: isExpanded ? 400 : 200 }}
className="bg-white rounded-lg"
/>
// ✅ Good: Use layout prop for automatic transform-based animation
<motion.div
layout
style={{ width: isExpanded ? 400 : 200 }}
className="bg-white rounded-lg"
/>
Do/Don’t List
| Do | Don’t |
|---|---|
Use layout prop for size/position changes | Animate width/height directly |
Add layout="position" for images and text | Let images distort during layout animations |
Use layoutScroll in scrollable containers | Forget scroll offset causes misaligned animations |
Set border-radius via style prop for scale correction | Use Tailwind classes for border-radius with layout |
Field Notes: Layout Animations
- What works: The
layoutprop handles 90% of layout animation needs automatically. List reordering withReorder.Groupfeels magical with minimal code. - What breaks: Elements with
display: inlinewon’t animate. Browsers don’t apply transforms to inline elements. Border and box-shadow can look stretched during scale animations. - How to fix it: Ensure elements are block or inline-block. For borders, use a wrapper element with padding instead. Set
border-radiusandbox-shadowvia thestyleprop for automatic scale correction. - Practical recommendation: Start with
layout={true}, then refine tolayout="position"for elements that shouldn’t scale. UseLayoutGroupwhen multiple unrelated components affect each other’s layout.
Exit Animations
Why don’t my Framer Motion exit animations work? Exit animations require three things: (1) AnimatePresence wraps the conditional (not inside it), (2) the motion component has a unique key prop, and (3) the motion component is a direct child of AnimatePresence. Missing any causes silent failures.
// ❌ Wrong: AnimatePresence inside the conditional
{isVisible && (
<AnimatePresence>
<motion.div exit={{ opacity: 0 }}>Content</motion.div>
</AnimatePresence>
)}
// ✅ Correct: AnimatePresence wraps the conditional
<AnimatePresence>
{isVisible && (
<motion.div key="content" exit={{ opacity: 0 }}>Content</motion.div>
)}
</AnimatePresence>
Basic Exit Animation Pattern
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
export function NotificationToast() {
const [isVisible, setIsVisible] = useState(true);
return (
<AnimatePresence>
{isVisible && (
<motion.div
key="toast"
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.9 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed bottom-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg"
>
<p>Changes saved successfully!</p>
<button
onClick={() => setIsVisible(false)}
className="ml-4 text-green-100 hover:text-white"
>
Dismiss
</button>
</motion.div>
)}
</AnimatePresence>
);
}
Modal with Backdrop Example
A complete modal implementation with coordinated exit animations:
import { motion, AnimatePresence } from "framer-motion";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.95, y: 20 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: "spring", damping: 25, stiffness: 300 }
},
exit: {
opacity: 0,
scale: 0.95,
y: 20,
transition: { duration: 0.2 }
},
};
export function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
key="backdrop"
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
onClick={onClose}
className="fixed inset-0 bg-black/50 z-40"
/>
<motion.div
key="modal"
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
className="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
{children}
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
Animated List Item Removal
For lists where items can be added or removed:
import { motion, AnimatePresence } from "framer-motion";
interface Notification {
id: string;
message: string;
}
export function NotificationStack({
notifications,
onDismiss
}: {
notifications: Notification[];
onDismiss: (id: string) => void;
}) {
return (
<div className="fixed top-4 right-4 space-y-2 z-50">
<AnimatePresence mode="popLayout">
{notifications.map((notification) => (
<motion.div
key={notification.id}
layout
initial={{ opacity: 0, x: 100, scale: 0.9 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 100, scale: 0.9 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="bg-white rounded-lg shadow-lg p-4 min-w-[300px]"
>
<p>{notification.message}</p>
<button
onClick={() => onDismiss(notification.id)}
className="text-gray-400 hover:text-gray-600 mt-2"
>
Dismiss
</button>
</motion.div>
))}
</AnimatePresence>
</div>
);
}
The mode="popLayout" prop ensures remaining items animate smoothly when one is removed.
Route Transitions
For page transitions in Next.js or React Router, wrap your route content:
"use client";
// In your layout or app component
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
export function PageTransitionLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<AnimatePresence mode="wait">
<motion.main
key={pathname}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.main>
</AnimatePresence>
);
}
Do/Don’t List
| Do | Don’t |
|---|---|
Always provide a unique key to exiting elements | Forget keys and wonder why exit doesn’t work |
Place AnimatePresence outside conditionals | Nest AnimatePresence inside the conditional |
Use mode="wait" for sequential transitions | Let enter/exit animations overlap unintentionally |
| Keep exit animations short (150-300ms) | Make users wait for long exit animations |
Field Notes: Exit Animations
- What works: Simple opacity and transform exit animations are reliable. The
mode="popLayout"option handles list removals elegantly. - What breaks: Non-motion elements inside
AnimatePresencecan prevent exit animations. HOC-wrapped components sometimes lose the exit animation. - How to fix it: Ensure all direct children of
AnimatePresenceare motion components. For HOCs, useforwardRefand ensure the motion component receives the key prop. - Practical recommendation: Create a reusable
FadePresencewrapper component that handles theAnimatePresenceboilerplate for common use cases like modals and toasts.
Performance Tips
Understanding browser rendering helps you make informed animation decisions. Janky animations hurt UX more than no animations.
The Rendering Pipeline
According to web.dev’s animation guide, browsers render in three main steps:
- Layout: Calculate element geometry (triggered by
width,height,margin,padding,top,left) - Paint: Draw pixels for each layer (triggered by
background-color,border-radius,box-shadow) - Composite: Combine layers on the GPU (triggered by
transform,opacity,filter)

Triggering an earlier step forces all subsequent steps to run. Composite-only animations are the most performant because they skip layout and paint entirely. They run on a separate compositor thread that isn’t blocked by JavaScript on the main thread. This is why Framer Motion’s layout prop uses transform internally, even when animating size changes.
Prefer Transform and Opacity
The Motion performance tier list categorizes animations by performance impact. Transform and opacity animations can run on the GPU compositor thread, remaining smooth even when the main thread is busy:
// ✅ S-Tier: Compositor-only, hardware accelerated
<motion.div
animate={{
x: 100, // transform: translateX
y: 50, // transform: translateY
scale: 1.1, // transform: scale
rotate: 45, // transform: rotate
opacity: 0.5 // opacity
}}
/>
// ❌ D-Tier: Triggers layout every frame
<motion.div
animate={{
width: 200, // Triggers layout
height: 100, // Triggers layout
marginLeft: 20 // Triggers layout
}}
/>
Understanding will-change
The CSS will-change property hints to the browser that an element will animate, allowing it to optimize ahead of time by promoting the element to its own compositor layer. Framer Motion applies this automatically during animations, but you can add it manually for elements that animate frequently:
// Framer Motion handles this automatically, but for custom CSS:
<motion.div
style={{ willChange: "transform" }}
whileHover={{ scale: 1.05 }}
/>
Caution: Don’t add will-change to many elements. Each creates a new GPU layer, consuming memory. Only use it on elements actively animating, and let Framer Motion manage it when possible.
Code Splitting with LazyMotion
Framer Motion is powerful, but that power comes with bytes. The full library is about 30-50kb gzipped. In production apps, shipping everything to the main bundle is a common mistake that hurts load-time performance.
Use LazyMotion with the m component to drastically reduce initial bundle size:
import { LazyMotion, domAnimation, m } from "framer-motion";
// 1. Replace <motion.div> with <m.div>
// 2. Wrap your app (or feature) in <LazyMotion>
function App({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
);
}
// Inside any component:
// ⚠️ Important: <m.div> will NOT animate unless wrapped in <LazyMotion> somewhere up the tree!
function FadeIn({ children }: { children: React.ReactNode }) {
return (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{children}
</m.div>
);
}
Feature Bundles:
domAnimation(about 15kb): Covers animations, transitions, and exit. Use this for most apps.domMax(about 27kb): Adds layout animations, drag, and more. Only import if you need these features.
For even more control, dynamically import features:
import { LazyMotion, m } from "framer-motion";
function App({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={() => import("framer-motion").then((mod) => mod.domMax)}>
{children}
</LazyMotion>
);
}
Imperative Animations with useAnimate
Sometimes declarative variants are too rigid. The useAnimate hook provides imperative control for complex sequences or one-off animations:
import { useAnimate } from "framer-motion";
export function CopyButton({ text }: { text: string }) {
const [scope, animate] = useAnimate();
const handleCopy = async () => {
await navigator.clipboard.writeText(text);
// Sequence: flash green, then reset
await animate(scope.current, { backgroundColor: "#22c55e" }, { duration: 0.1 });
await animate(scope.current, { backgroundColor: "#ffffff" }, { duration: 0.3 });
};
return (
<button ref={scope} onClick={handleCopy} className="px-4 py-2 rounded border">
Copy
</button>
);
}
Use useAnimate when:
- You need to chain animations that depend on async operations
- State management for animation states feels like overkill
- The animation fires once and doesn’t need to stay in a state
Reduce Re-renders
Motion components can trigger unnecessary re-renders. Extract motion components and memoize when appropriate:
import { motion } from "framer-motion";
import { memo } from "react";
// Extract animation variants outside component
const fadeInVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
// Memoize motion components that receive stable props
const AnimatedCard = memo(function AnimatedCard({
children
}: {
children: React.ReactNode
}) {
return (
<motion.div
variants={fadeInVariants}
initial="hidden"
animate="visible"
className="bg-white rounded-lg p-4 shadow"
>
{children}
</motion.div>
);
});
Spring Configuration
Springs create natural-feeling animations, but extreme values hurt performance:

// ✅ Good: Reasonable spring values
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 300, // 100-500 is typical
damping: 25 // 10-40 is typical
}}
/>
// ❌ Problematic: Very high stiffness causes many frames
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 10000, // Too high
damping: 5 // Too low (bouncy)
}}
/>
Transform Origin
When animating scale or rotate, the transform-origin determines the pivot point. By default, transforms originate from the element’s center. Override with the style prop:
// Scale from top-left corner
<motion.div
style={{ transformOrigin: "top left" }}
whileHover={{ scale: 1.1 }}
/>
// Rotate from bottom center (like a pendulum)
<motion.div
style={{ transformOrigin: "bottom center" }}
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ repeat: Infinity, duration: 2 }}
/>
Programmatic Control with useAnimationControls
For animations triggered by external events (not React state), use useAnimationControls:
import { motion, useAnimationControls } from "framer-motion";
import { useEffect } from "react";
export function ShakeOnError({ error }: { error: string | null }) {
const controls = useAnimationControls();
useEffect(() => {
if (error) {
controls.start({
x: [0, -10, 10, -10, 10, 0],
transition: { duration: 0.4 }
});
}
}, [error, controls]);
return (
<motion.div animate={controls} className="p-4 border rounded">
<input type="text" />
{error && <p className="text-red-500">{error}</p>}
</motion.div>
);
}
React 18 Concurrent Mode Compatibility
Framer Motion v10+ is fully compatible with React 18’s Concurrent Mode and Strict Mode. Animations work correctly with <Suspense> boundaries and streaming SSR. If you’re using React 17, stick with Framer Motion v6-v9 for best compatibility.
Avoid Expensive Effects
Some CSS properties are particularly costly to animate:
// ❌ Expensive: Large blur radius
<motion.div
animate={{ filter: "blur(50px)" }} // Very expensive
/>
// ❌ Expensive: Box shadow changes
<motion.div
animate={{
boxShadow: "0 25px 50px rgba(0,0,0,0.5)" // Triggers paint
}}
/>
// ✅ Better: Animate a pseudo-element's opacity instead
// Pre-render the shadow, then fade it in/out
Accessibility: Respecting Reduced Motion
Users can enable “Reduce Motion” in their operating system settings to minimize animations that may cause discomfort. Framer Motion provides two approaches to respect this preference.
Global Configuration with MotionConfig:
import { MotionConfig } from "framer-motion";
export function App({ children }: { children: React.ReactNode }) {
return (
<MotionConfig reducedMotion="user">
{children}
</MotionConfig>
);
}
Setting reducedMotion="user" automatically disables transform and layout animations while preserving opacity and color transitions. This is the recommended approach from the official docs. For a deeper dive into building accessible interfaces, see building interactive UI components.
Custom Control with useReducedMotion:
For more granular control, use the useReducedMotion hook:
import { motion, useReducedMotion } from "framer-motion";
export function Sidebar({ isOpen }: { isOpen: boolean }) {
const shouldReduceMotion = useReducedMotion();
// Replace slide animation with fade for reduced motion users
const closedX = shouldReduceMotion ? 0 : "-100%";
return (
<motion.aside
animate={{
opacity: isOpen ? 1 : 0,
x: isOpen ? 0 : closedX,
}}
transition={{
duration: shouldReduceMotion ? 0.1 : 0.3
}}
className="fixed left-0 top-0 h-full w-64 bg-gray-900"
>
{/* Sidebar content */}
</motion.aside>
);
}
Motion Accessibility Checklist:
- Wrap app in
<MotionConfig reducedMotion="user">for automatic handling - Use
useReducedMotion()for custom reduced-motion alternatives - Replace x/y transforms with opacity fades for reduced motion
- Disable parallax and auto-playing animations
- Keep essential transitions (like focus indicators) even with reduced motion
- Test with “Reduce Motion” enabled in your OS settings
What Teams Run Into
Based on common issues developers encounter in production:
-
SSR Hydration Mismatches: In Next.js, the
data-projection-idprop can differ between server and client renders, causing console warnings. This is cosmetic but annoying. Suppress withsuppressHydrationWarningon the motion component or ensure animations only run client-side. For a deeper understanding of server vs client components, see React Server Components guide. -
Initial Animation Flash: Components may flash their initial state before animating. Use
initial={false}to skip mount animations, or ensure the initial state matches the server-rendered state. -
Exit Animations Not Playing: The most common issue. Usually caused by missing keys,
AnimatePresenceplacement, or non-motion children. Always verify the three requirements: unique key, direct child, correct wrapper placement. -
Layout Animation Jank: Large layout shifts can cause jank even with the
layoutprop. Break complex layouts into smaller animated sections. UseLayoutGroupto coordinate animations across components that don’t share a parent. -
Reduced Motion Breaking Animations: When
reducedMotion="user"is set, transform animations are disabled. If your animation relies entirely on transforms, it may appear to do nothing. Always include opacity changes as a fallback. -
Route Transition Conflicts: Page transitions with
AnimatePresence mode="wait"can feel slow because the old page must fully exit before the new one enters. Considermode="popLayout"or crossfade patterns for snappier transitions. -
Memory Leaks with Scroll Animations: Scroll-linked animations that don’t clean up properly can cause memory issues. Use Framer Motion’s built-in scroll functions rather than manual
scrollTopreads. -
Mobile Performance Issues: Animations smooth on desktop may stutter on mobile. Test on real devices, reduce animation complexity, and avoid large blur values.
Mini Case Study: Toast Notification System
Problem: Building a toast system where notifications stack, can be dismissed individually, and animate smoothly when others are removed.
Approach: Use AnimatePresence with mode="popLayout" and the layout prop on each toast. This ensures remaining toasts animate into their new positions when one is dismissed.
import { motion, AnimatePresence } from "framer-motion";
interface Toast {
id: string;
message: string;
type: "success" | "error" | "info";
}
export function ToastContainer({
toasts,
onDismiss
}: {
toasts: Toast[];
onDismiss: (id: string) => void;
}) {
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
<AnimatePresence mode="popLayout">
{toasts.map((toast) => (
<motion.div
key={toast.id}
layout
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
className={`px-4 py-3 rounded-lg shadow-lg min-w-[280px] ${
toast.type === "success" ? "bg-green-500" :
toast.type === "error" ? "bg-red-500" : "bg-blue-500"
} text-white`}
>
<div className="flex justify-between items-center">
<span>{toast.message}</span>
<button
onClick={() => onDismiss(toast.id)}
className="ml-4 opacity-70 hover:opacity-100"
>
×
</button>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
);
}
Tradeoffs: The layout prop adds slight overhead for position calculations. For very high-frequency toast updates, consider debouncing or limiting the maximum visible toasts.
Mini Case Study: Dashboard Sidebar with Content Reflow
Problem: A collapsible sidebar that, when toggled, should smoothly animate while the main content area expands to fill the space.
Approach: Use LayoutGroup to coordinate animations between the sidebar and content area, even though they’re separate components.
import { motion, LayoutGroup } from "framer-motion";
import { useState } from "react";
export function DashboardLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<LayoutGroup>
<div className="flex min-h-screen">
<motion.aside
layout
animate={{ width: sidebarOpen ? 256 : 64 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="bg-gray-900 text-white overflow-hidden"
>
<motion.div layout="position" className="p-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="w-full text-left"
>
{sidebarOpen ? "← Collapse" : "→"}
</button>
</motion.div>
{sidebarOpen && (
<motion.nav
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="p-4"
>
{/* Navigation items */}
</motion.nav>
)}
</motion.aside>
<motion.main layout className="flex-1 p-8">
{children}
</motion.main>
</div>
</LayoutGroup>
);
}
Tradeoffs: LayoutGroup synchronizes layout measurements across components, which adds overhead. Only use it when components genuinely affect each other’s layout.
Do/Don’t List
| Do | Don’t |
|---|---|
Animate transform and opacity for best performance | Animate width, height, or margin directly |
Use MotionConfig reducedMotion="user" globally | Ignore accessibility preferences |
| Extract variants outside components | Define animation objects inline in render |
| Test on real mobile devices | Assume desktop performance equals mobile |
Field Notes: Performance
- What works: Sticking to transform and opacity animations handles 95% of use cases with excellent performance. The
layoutprop is remarkably efficient for what it accomplishes. - What breaks: Animating CSS variables, especially inherited ones, can force style recalculations across thousands of elements. Large blur values (>10px) are expensive. Animating many elements simultaneously without virtualization causes frame drops.
- How to fix it: Profile with Chrome DevTools Performance tab. Look for long “Recalculate Style” or “Layout” entries. Replace CSS variable animations with targeted style updates. Use
will-changesparingly and only on elements about to animate. - Practical recommendation: Create a performance budget: no animation should cause frame drops on a mid-range phone. Test with CPU throttling enabled in DevTools.
Common Errors and Fixes
Quick reference for the most common Framer Motion issues.
”Cannot read property ‘getContext’ of null” / createContext Error
Cause: Missing "use client" directive in Next.js App Router.
Fix: Add "use client" at the top of any file importing from framer-motion.
Exit Animation Not Playing
Checklist:
- ✅
AnimatePresencewraps the conditional (not inside it) - ✅ Motion component has a unique
keyprop - ✅ Motion component is a direct child of
AnimatePresence
Layout Animation Causes Content to Distort
Fix: Use layout="position" on text and images that shouldn’t scale:
<motion.div layout>
<motion.img layout="position" src="/avatar.jpg" />
<motion.h3 layout="position">Title</motion.h3>
</motion.div>
Hydration Mismatch Warning
Fix: Add suppressHydrationWarning or delay animation until after mount:
<motion.div layout suppressHydrationWarning />
Variant Inheritance Not Working
Cause: Child has explicit animate prop, breaking inheritance.
Fix: Remove animate from children; only define variants.
Animation Janky on Mobile
Fix:
- Only animate
transformandopacity - Keep spring stiffness 100-500, damping 10-40
- Avoid blur values above 10px
- Test with Chrome DevTools CPU throttling (4x)
useReducedMotion Returns Null
Fix: Handle the null case or use MotionConfig:
const shouldReduceMotion = useReducedMotion();
const duration = shouldReduceMotion ?? false ? 0.1 : 0.4;
Quick Reference
Essential Imports
// Core
import { motion, AnimatePresence } from "framer-motion";
// Hooks - Values & Transforms
import { useMotionValue, useTransform, useSpring, useVelocity } from "framer-motion";
// Hooks - Scroll & View
import { useScroll, useInView } from "framer-motion";
// Hooks - Animation Control
import { useAnimate, useAnimationControls, useReducedMotion } from "framer-motion";
// Optimization
import { LazyMotion, domAnimation, domMax, m } from "framer-motion";
// Layout
import { LayoutGroup, Reorder } from "framer-motion";
// Config
import { MotionConfig } from "framer-motion";
// Utilities
import { animate, stagger } from "framer-motion";
Core Props
| Prop | Type | Description |
|---|---|---|
initial | object | string | false | Starting animation state |
animate | object | string | Target animation state |
exit | object | string | Exit animation (requires AnimatePresence) |
transition | object | Animation timing/physics config |
variants | object | Named animation states |
layout | boolean | “position” | “size” | Enable layout animations |
layoutId | string | Shared element transitions |
whileHover | object | Animation on hover |
whileTap | object | Animation on press |
whileInView | object | Animation when in viewport |
whileDrag | object | Animation while dragging |
whileFocus | object | Animation when focused |
drag | boolean | “x” | “y” | Enable dragging |
dragConstraints | object | RefObject | Drag boundaries |
onAnimationComplete | function | Callback when animation finishes |
Hooks Reference
| Hook | Purpose | Returns |
|---|---|---|
useMotionValue(initial) | Create animatable value outside React state | MotionValue |
useTransform(value, input, output) | Derive one motion value from another | MotionValue |
useSpring(value, config) | Add spring physics to a motion value | MotionValue |
useVelocity(value) | Track velocity of a motion value | MotionValue |
useScroll(options) | Track scroll progress | { scrollX, scrollY, scrollXProgress, scrollYProgress } |
useInView(ref, options) | Detect element visibility | boolean |
useAnimate() | Imperative animation control | [scope, animate] |
useAnimationControls() | Programmatic animation triggers | AnimationControls |
useReducedMotion() | Check user’s motion preference | boolean | null |
Transition Types
// Spring (default for physical properties)
transition={{ type: "spring", stiffness: 300, damping: 25, mass: 1 }}
// Tween (duration-based)
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
transition={{ type: "tween", duration: 0.5, ease: [0.43, 0.13, 0.23, 0.96] }} // Custom bezier
// Inertia (for drag momentum)
transition={{ type: "inertia", velocity: 50, power: 0.8 }}
// Keyframes
animate={{ x: [0, 100, 50] }}
transition={{ duration: 1, times: [0, 0.5, 1], ease: "easeInOut" }}
// Stagger (with animate function)
animate("li", { opacity: 1 }, { delay: stagger(0.1) })
AnimatePresence Modes
| Mode | Behavior | Use Case |
|---|---|---|
"sync" (default) | Enter and exit run simultaneously | Crossfade effects |
"wait" | Exit completes before enter starts | Page transitions |
"popLayout" | Exiting elements removed from layout flow | List item removal |
Performance Tiers
| Tier | Properties | Performance |
|---|---|---|
| S-Tier | opacity, transform (x, y, scale, rotate) | GPU compositor, 60fps |
| A-Tier | filter (small values), clipPath | Paint only, usually smooth |
| C-Tier | backgroundColor, borderRadius | Paint, may cause jank |
| D-Tier | width, height, margin, padding | Layout + Paint, avoid |
Key Takeaways
- Replace
<div>with<motion.div>and addinitial,animate,transitionprops - Use variants for orchestrated animations across parent-child components with
staggerChildren - AnimatePresence requires three things: wrap the conditional, unique key, direct child
- Layout prop enables automatic FLIP animations. Use
layout="position"for text/images - Stick to transform and opacity for 60fps performance on mobile
- Use
MotionConfig reducedMotion="user"for automatic accessibility compliance - LazyMotion cuts bundle size in half. Use
domAnimation(about 15kb) for most apps
What to Build Next
Now that you understand Framer Motion’s core patterns, try building:
- A toast notification system with stacking, dismissal, and smooth reordering (uses
AnimatePresence mode="popLayout"+layout) - A shared element transition between a grid and detail view (uses
layoutId) - A scroll-driven parallax hero with multiple layers moving at different speeds (uses
useScroll+useTransform)
Each project exercises different Framer Motion capabilities and will solidify your understanding of when to reach for each tool.
Version Compatibility
Framer Motion was rebranded to “Motion” in late 2024, though the npm package remains framer-motion. Here’s what you need to know:
| Version | React Support | Key Features | Notes |
|---|---|---|---|
| v12.x (current) | React 18-19 | Full feature set, improved performance | Recommended for new projects |
| v11.x | React 18 | Stable, production-ready | Safe upgrade from v10 |
| v10.x | React 18 | useAnimate, improved LazyMotion | Minimum for this guide |
| v6-9 | React 17-18 | Core features, older API | Legacy support |
Migration notes:
- v10+ requires React 18 for full compatibility
- The
motion.devdomain hosts the new documentation - Import paths remain unchanged (
framer-motion) - Most v6+ code works in v12 with minimal changes
Check your version: npm list framer-motion