Motion for React: Build Production-Grade Animations, Gestures, and Transitions

Introduction
Animations transform a good UI into a great one. They guide users through state changes, provide feedback on interactions, and make applications feel alive. Motion (formerly Framer Motion) is the most popular animation library for React, used by companies like Vercel, Linear, and Stripe to create fluid, performant interfaces.
In this tutorial, you will learn Motion from the ground up. Starting with simple fade-ins, you will progress through gestures, layout animations, scroll-driven effects, and complex orchestrated sequences. By the end, you will have built a fully animated product card gallery with interactive hover effects, smooth layout transitions, and scroll-triggered reveals.
What You Will Learn
- Install and configure Motion in a Next.js or React project
- Animate components with the
motioncomponent andanimateprop - Create interactive gestures: hover, tap, drag, and focus
- Build layout animations that smoothly transition between states
- Use
AnimatePresencefor enter and exit animations - Implement scroll-driven animations with
useScrollanduseTransform - Orchestrate complex sequences with variants and stagger effects
- Optimize animation performance for production
Prerequisites
Before starting, make sure you have:
- Node.js 18+ installed on your machine
- Basic knowledge of React and TypeScript
- Familiarity with CSS transforms and transitions (helpful but not required)
- A code editor (VS Code recommended)
Step 1: Project Setup
Create a new Next.js project and install Motion:
npx create-next-app@latest motion-demo --typescript --tailwind --app --src-dir
cd motion-demoInstall the Motion library:
npm install motionMotion v11+ is published under the motion package name (the framer-motion package still works but redirects to motion). The API is the same — only the package name changed.
Verify your installation by checking package.json:
{
"dependencies": {
"motion": "^11.18.0",
"next": "^15.2.0",
"react": "^19.0.0"
}
}Step 2: Your First Animation
Replace the contents of src/app/page.tsx with a simple animated component:
"use client";
import { motion } from "motion/react";
export default function Home() {
return (
<main className="flex min-h-screen items-center justify-center bg-gray-950">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="rounded-2xl bg-white p-8 shadow-2xl"
>
<h1 className="text-2xl font-bold text-gray-900">
Hello, Motion!
</h1>
<p className="mt-2 text-gray-600">
This card faded in and slid up smoothly.
</p>
</motion.div>
</main>
);
}Run the dev server with npm run dev and open http://localhost:3000. You will see the card fade in and slide up when the page loads.
How It Works
motion.divis a drop-in replacement for<div>that supports animation propsinitialdefines the starting state (invisible, shifted 40px down)animatedefines the target state (fully visible, original position)transitioncontrols the timing and easing
Motion automatically interpolates between initial and animate values, handling all the requestAnimationFrame logic and GPU-accelerated transforms for you.
Step 3: Spring Physics and Transition Types
Motion supports three transition types: tween (duration-based), spring (physics-based), and inertia (momentum-based). Spring animations feel the most natural for UI interactions.
"use client";
import { motion } from "motion/react";
export default function SpringDemo() {
return (
<div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
{/* Tween: predictable, duration-based */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "tween", duration: 0.5 }}
className="h-24 w-24 rounded-2xl bg-blue-500"
/>
{/* Spring: natural, physics-based */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
className="h-24 w-24 rounded-2xl bg-green-500"
/>
{/* Spring with bounce */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", bounce: 0.5 }}
className="h-24 w-24 rounded-2xl bg-purple-500"
/>
</div>
);
}Spring Parameters
| Parameter | Description | Default |
|---|---|---|
stiffness | How stiff the spring is (higher = snappier) | 100 |
damping | How much resistance (higher = less oscillation) | 10 |
mass | Weight of the animated object | 1 |
bounce | Shorthand: 0 = no bounce, 1 = very bouncy | 0.25 |
Pro tip: For most UI animations, type: "spring" with default values works perfectly. Only tweak parameters when the default feel does not match your design intent.
Step 4: Interactive Gestures
Motion makes it trivial to add hover, tap, drag, and focus animations. These gesture props respond to user interactions without any state management.
Hover and Tap
"use client";
import { motion } from "motion/react";
export default function GestureDemo() {
return (
<div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
<motion.button
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6" }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="rounded-xl bg-blue-600 px-8 py-4 text-lg font-semibold text-white"
>
Press Me
</motion.button>
<motion.div
whileHover={{
rotate: 5,
boxShadow: "0 25px 50px -12px rgba(0,0,0,0.5)",
}}
whileTap={{ rotate: -5, scale: 0.9 }}
className="flex h-32 w-32 cursor-pointer items-center justify-center rounded-2xl bg-gradient-to-br from-pink-500 to-orange-400 text-white font-bold"
>
Interact
</motion.div>
</div>
);
}Drag
Motion supports full drag with constraints, elastic boundaries, and momentum:
"use client";
import { motion } from "motion/react";
import { useRef } from "react";
export default function DragDemo() {
const constraintsRef = useRef<HTMLDivElement>(null);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950">
{/* Drag boundary container */}
<motion.div
ref={constraintsRef}
className="relative flex h-96 w-96 items-center justify-center rounded-3xl border-2 border-dashed border-gray-700"
>
<p className="absolute top-4 text-sm text-gray-500">
Drag the ball around
</p>
<motion.div
drag
dragConstraints={constraintsRef}
dragElastic={0.2}
dragTransition={{ bounceStiffness: 300, bounceDamping: 20 }}
whileDrag={{ scale: 1.2, cursor: "grabbing" }}
className="h-20 w-20 cursor-grab rounded-full bg-gradient-to-br from-violet-500 to-cyan-400 shadow-lg"
/>
</motion.div>
</div>
);
}Key drag props:
dragenables dragging (true,"x", or"y"for axis lock)dragConstraintslimits movement (ref or pixel values)dragElasticcontrols how far it can be dragged past constraints (0-1)whileDragapplies styles while dragging
Step 5: Variants and Orchestration
When you need to animate multiple children in sequence, variants let you define named animation states and propagate them down the component tree.
"use client";
import { motion } from "motion/react";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20, scale: 0.95 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
const features = [
{ title: "Fast", description: "GPU-accelerated animations at 60fps" },
{ title: "Simple", description: "Declarative API with zero boilerplate" },
{ title: "Powerful", description: "Gestures, layout, and scroll support" },
{ title: "Tiny", description: "Tree-shakeable, only ship what you use" },
];
export default function StaggerDemo() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid max-w-2xl grid-cols-2 gap-4"
>
{features.map((feature) => (
<motion.div
key={feature.title}
variants={itemVariants}
className="rounded-xl bg-gray-900 p-6 border border-gray-800"
>
<h3 className="text-lg font-bold text-white">{feature.title}</h3>
<p className="mt-1 text-sm text-gray-400">{feature.description}</p>
</motion.div>
))}
</motion.div>
</div>
);
}How Variants Propagate
- The parent
motion.divsetsinitial="hidden"andanimate="visible" - Children with
variantsautomatically inherit these states staggerChildren: 0.1adds a 100ms delay between each child animationdelayChildren: 0.2waits 200ms before the first child starts
This pattern is perfect for animating lists, grids, navigation menus, and any group of related elements.
Step 6: AnimatePresence for Exit Animations
React removes elements from the DOM instantly. AnimatePresence intercepts this and allows exit animations to complete before removal.
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
const notifications = [
{ id: 1, text: "New message received", color: "bg-blue-500" },
{ id: 2, text: "File uploaded successfully", color: "bg-green-500" },
{ id: 3, text: "Payment processed", color: "bg-purple-500" },
{ id: 4, text: "Deployment complete", color: "bg-orange-500" },
];
export default function ExitDemo() {
const [items, setItems] = useState(notifications);
const removeItem = (id: number) => {
setItems((prev) => prev.filter((item) => item.id !== id));
};
const resetItems = () => setItems(notifications);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="w-full max-w-md space-y-3">
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, x: -40, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 40, scale: 0.95 }}
transition={{ type: "spring", stiffness: 350, damping: 25 }}
className="flex items-center justify-between rounded-xl bg-gray-900 p-4 border border-gray-800"
>
<div className="flex items-center gap-3">
<div className={`h-3 w-3 rounded-full ${item.color}`} />
<span className="text-white">{item.text}</span>
</div>
<button
onClick={() => removeItem(item.id)}
className="text-gray-500 hover:text-red-400 transition-colors"
>
Dismiss
</button>
</motion.div>
))}
</AnimatePresence>
{items.length === 0 && (
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onClick={resetItems}
className="w-full rounded-xl bg-blue-600 py-3 text-white font-semibold"
>
Reset Notifications
</motion.button>
)}
</div>
</div>
);
}AnimatePresence Modes
| Mode | Behavior |
|---|---|
"sync" (default) | Exit and enter animations happen simultaneously |
"wait" | Wait for exit to complete before entering |
"popLayout" | Exiting elements are popped out of the layout flow |
The layout prop on each item ensures that remaining items smoothly reposition when a sibling is removed.
Step 7: Layout Animations
Layout animations are one of Motion's most powerful features. By adding the layout prop, elements smoothly animate between different CSS layouts — no manual coordinate calculations needed.
"use client";
import { useState } from "react";
import { motion } from "motion/react";
export default function LayoutDemo() {
const [selected, setSelected] = useState<string | null>(null);
const items = ["Design", "Develop", "Deploy", "Monitor"];
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="flex gap-2 rounded-2xl bg-gray-900 p-2">
{items.map((item) => (
<button
key={item}
onClick={() => setSelected(item)}
className="relative rounded-xl px-6 py-3 text-sm font-medium text-white outline-none"
>
{selected === item && (
<motion.div
layoutId="active-tab"
className="absolute inset-0 rounded-xl bg-blue-600"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
<span className="relative z-10">{item}</span>
</button>
))}
</div>
</div>
);
}The Magic of layoutId
When you give multiple elements the same layoutId, Motion treats them as the same element across renders. When one unmounts and another mounts, Motion animates between their positions and sizes automatically. This is how you create:
- Shared element transitions (like the tab indicator above)
- Card-to-modal expansions
- List reordering animations
Card Expansion Example
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
const cards = [
{ id: "1", title: "Analytics", description: "Track user behavior and engagement metrics across your platform.", color: "from-blue-500 to-cyan-400" },
{ id: "2", title: "Security", description: "End-to-end encryption and compliance-ready infrastructure.", color: "from-purple-500 to-pink-400" },
{ id: "3", title: "Speed", description: "Edge-deployed globally with sub-100ms response times.", color: "from-orange-500 to-yellow-400" },
];
export default function CardExpansion() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedCard = cards.find((c) => c.id === selectedId);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="grid grid-cols-3 gap-4">
{cards.map((card) => (
<motion.div
key={card.id}
layoutId={`card-${card.id}`}
onClick={() => setSelectedId(card.id)}
className={`cursor-pointer rounded-2xl bg-gradient-to-br ${card.color} p-6`}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
>
<motion.h3
layoutId={`title-${card.id}`}
className="text-xl font-bold text-white"
>
{card.title}
</motion.h3>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && selectedCard && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.5 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="fixed inset-0 bg-black"
/>
<motion.div
layoutId={`card-${selectedId}`}
className={`fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-2xl bg-gradient-to-br ${selectedCard.color} p-8`}
>
<motion.h3
layoutId={`title-${selectedId}`}
className="text-2xl font-bold text-white"
>
{selectedCard.title}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="mt-4 text-white/90"
>
{selectedCard.description}
</motion.p>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="mt-6 rounded-lg bg-white/20 px-4 py-2 text-white font-medium backdrop-blur"
>
Close
</motion.button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}This creates a smooth expansion from card to modal without any manual position tracking. Motion calculates the FLIP animation automatically.
Step 8: Scroll-Driven Animations
Motion provides hooks for creating animations that respond to scroll position. This is perfect for hero sections, progress indicators, parallax effects, and reveal-on-scroll patterns.
Scroll Progress Indicator
"use client";
import { motion, useScroll, useSpring } from "motion/react";
export default function ScrollProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});
return (
<>
<motion.div
style={{ scaleX }}
className="fixed top-0 left-0 right-0 z-50 h-1 origin-left bg-blue-500"
/>
<main className="space-y-8 p-8">
{Array.from({ length: 20 }).map((_, i) => (
<div
key={i}
className="mx-auto h-48 max-w-2xl rounded-2xl bg-gray-900 border border-gray-800"
/>
))}
</main>
</>
);
}Scroll-Triggered Reveal
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
function RevealSection({ 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: 60 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }}
transition={{ duration: 0.7, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
export default function ScrollReveal() {
const sections = [
{ title: "Design", text: "Create beautiful interfaces with Motion" },
{ title: "Animate", text: "Add fluid animations to every interaction" },
{ title: "Ship", text: "Deploy performant animations to production" },
];
return (
<main className="min-h-screen bg-gray-950 p-8">
<div className="mx-auto max-w-2xl space-y-32 py-32">
{sections.map((section) => (
<RevealSection key={section.title}>
<h2 className="text-4xl font-bold text-white">{section.title}</h2>
<p className="mt-4 text-xl text-gray-400">{section.text}</p>
</RevealSection>
))}
</div>
</main>
);
}Parallax Effect
"use client";
import { motion, useScroll, useTransform } from "motion/react";
import { useRef } from "react";
export default function ParallaxHero() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], [0, 200]);
const opacity = useTransform(scrollYProgress, [0, 0.8], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.85]);
return (
<div ref={ref} className="relative h-screen overflow-hidden bg-gray-950">
<motion.div
style={{ y, opacity, scale }}
className="flex h-full flex-col items-center justify-center"
>
<h1 className="text-6xl font-bold text-white">Parallax Hero</h1>
<p className="mt-4 text-xl text-gray-400">Scroll down to see the effect</p>
</motion.div>
</div>
);
}Step 9: Building a Complete Animated Product Gallery
Let us combine everything into a real-world component — an animated product card gallery with staggered entrance, hover effects, and expandable detail views.
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
interface Product {
id: string;
name: string;
price: string;
category: string;
gradient: string;
description: string;
}
const products: Product[] = [
{
id: "1",
name: "Pro Headphones",
price: "$299",
category: "Audio",
gradient: "from-violet-600 to-indigo-600",
description:
"Premium wireless headphones with active noise cancellation, 40-hour battery life, and studio-quality sound.",
},
{
id: "2",
name: "Ultra Monitor",
price: "$899",
category: "Display",
gradient: "from-cyan-600 to-blue-600",
description:
"32-inch 4K display with HDR1000, 165Hz refresh rate, and factory-calibrated colors for creative professionals.",
},
{
id: "3",
name: "Mech Keyboard",
price: "$179",
category: "Input",
gradient: "from-orange-600 to-red-600",
description:
"Hot-swappable mechanical keyboard with per-key RGB, gasket mount design, and premium PBT keycaps.",
},
{
id: "4",
name: "Ergo Mouse",
price: "$129",
category: "Input",
gradient: "from-emerald-600 to-teal-600",
description:
"Ergonomic vertical mouse with 8K DPI sensor, Bluetooth 5.3, and customizable side buttons.",
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
};
const cardVariants = {
hidden: { opacity: 0, y: 30, scale: 0.96 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
export default function ProductGallery() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedProduct = products.find((p) => p.id === selectedId);
return (
<div className="min-h-screen bg-gray-950 p-8">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-12 text-center text-4xl font-bold text-white"
>
Featured Products
</motion.h1>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="mx-auto grid max-w-5xl grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4"
>
{products.map((product) => (
<motion.div
key={product.id}
layoutId={`product-${product.id}`}
variants={cardVariants}
onClick={() => setSelectedId(product.id)}
whileHover={{ y: -8, transition: { type: "spring", stiffness: 400 } }}
whileTap={{ scale: 0.97 }}
className="cursor-pointer overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
>
<div
className={`h-40 bg-gradient-to-br ${product.gradient} flex items-center justify-center`}
>
<motion.span
layoutId={`emoji-${product.id}`}
className="text-5xl"
>
{product.category === "Audio"
? "🎧"
: product.category === "Display"
? "🖥️"
: product.category === "Input"
? "⌨️"
: "🖱️"}
</motion.span>
</div>
<div className="p-4">
<motion.h3
layoutId={`name-${product.id}`}
className="font-bold text-white"
>
{product.name}
</motion.h3>
<motion.span
layoutId={`price-${product.id}`}
className="mt-1 block text-sm text-gray-400"
>
{product.price}
</motion.span>
</div>
</motion.div>
))}
</motion.div>
<AnimatePresence>
{selectedId && selectedProduct && (
<>
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="fixed inset-0 z-40 bg-black"
/>
<motion.div
key="modal"
layoutId={`product-${selectedId}`}
className="fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
>
<div
className={`h-48 bg-gradient-to-br ${selectedProduct.gradient} flex items-center justify-center`}
>
<motion.span
layoutId={`emoji-${selectedId}`}
className="text-7xl"
>
{selectedProduct.category === "Audio"
? "🎧"
: selectedProduct.category === "Display"
? "🖥️"
: selectedProduct.category === "Input"
? "⌨️"
: "🖱️"}
</motion.span>
</div>
<div className="p-6">
<motion.h3
layoutId={`name-${selectedId}`}
className="text-2xl font-bold text-white"
>
{selectedProduct.name}
</motion.h3>
<motion.span
layoutId={`price-${selectedId}`}
className="mt-1 block text-lg text-gray-400"
>
{selectedProduct.price}
</motion.span>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
className="mt-4 text-gray-300"
>
{selectedProduct.description}
</motion.p>
<motion.button
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0, transition: { delay: 0.3 } }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="mt-6 w-full rounded-xl bg-white/10 py-3 text-white font-semibold hover:bg-white/20 transition-colors"
>
Close
</motion.button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}This gallery demonstrates:
- Staggered entrance with
containerVariantsandcardVariants - Hover lift effect with
whileHover - Shared layout transition from card to modal with
layoutId - Exit animation with
AnimatePresence - Delayed content reveal inside the expanded modal
Step 10: Performance Optimization
Motion is designed to be performant, but there are patterns to keep your animations smooth in production.
1. Animate Transform and Opacity Only
GPU-accelerated properties (transform and opacity) are the cheapest to animate. Avoid animating width, height, padding, or top/left — use scale, x, y, and opacity instead.
// Slow: triggers layout recalculation
<motion.div animate={{ width: 200, height: 200 }} />
// Fast: GPU-accelerated transform
<motion.div animate={{ scale: 1.5 }} />2. Use layout Wisely
The layout prop is powerful but expensive for large lists. If you only need position changes, use layout="position" instead of layout={true}:
// Animates position, size, and border-radius
<motion.div layout />
// Only animates position (cheaper)
<motion.div layout="position" />3. Reduce Motion for Accessibility
Always respect the user's reduced motion preference:
"use client";
import { motion, useReducedMotion } from "motion/react";
export default function AccessibleAnimation() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0.01 : 0.6 }}
className="rounded-2xl bg-gray-900 p-8"
>
<p className="text-white">Respects prefers-reduced-motion</p>
</motion.div>
);
}4. Lazy Load Motion Components
If animations are only used on specific pages, dynamically import Motion to reduce bundle size:
import dynamic from "next/dynamic";
const AnimatedHero = dynamic(() => import("@/components/AnimatedHero"), {
ssr: false,
loading: () => <div className="h-screen bg-gray-950" />,
});5. Use will-change Sparingly
Motion already applies will-change: transform when needed. Do not add it manually unless you have profiled and confirmed it helps — overuse consumes GPU memory.
Troubleshooting
Common Issues
Animation flickers on first render with Next.js App Router
Add "use client" to any component using Motion. Server Components cannot use Motion hooks or gesture props.
Layout animation causes elements to jump
Ensure parent elements have stable dimensions. Avoid percentage-based widths on elements with layout — use fixed or flex-based sizing instead.
Exit animation does not play
Make sure:
AnimatePresencewraps the conditional elements- Each child has a unique
keyprop - The
exitprop is defined on the direct child ofAnimatePresence
Bundle size is too large
Motion v11+ is fully tree-shakeable. Only import what you use:
// Good: only imports what's needed
import { motion, AnimatePresence } from "motion/react";
// Avoid: importing everything
import * as Motion from "motion/react";Next Steps
Now that you understand Motion fundamentals, here are some directions to explore:
- Page transitions — combine Motion with Next.js layout transitions for smooth navigation
- SVG animations — use
motion.pathandpathLengthfor drawing effects - 3D transforms — use
rotateX,rotateY, andperspectivefor card flip effects - Scroll-linked parallax — build immersive landing pages with
useScrollanduseTransform - Reusable animation hooks — extract patterns into custom hooks like
useFadeInoruseStagger
Conclusion
Motion brings production-grade animations to React with a simple, declarative API. You learned how to create basic animations with springs and tweens, build interactive gestures for hover, tap, and drag, orchestrate complex sequences with variants, handle enter and exit transitions with AnimatePresence, animate layout changes automatically, and build scroll-driven effects.
The key to great UI animation is restraint — animate with purpose, respect reduced motion preferences, and always optimize for performance. Start with the product gallery you built in this tutorial and experiment with adding Motion to your own projects.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Build a Real-Time Full-Stack App with Convex and Next.js 15
Learn how to build a real-time full-stack application using Convex and Next.js 15. This tutorial covers schema design, queries, mutations, real-time subscriptions, authentication, and file uploads — all with end-to-end type safety.

Build a Full-Stack App with Firebase and Next.js 15: Auth, Firestore & Real-Time
Learn how to build a full-stack app with Next.js 15 and Firebase. This guide covers authentication, Firestore, real-time updates, Server Actions, and deployment to Vercel.

Authenticate Your Next.js 15 App with Auth.js v5: Email, OAuth, and Role-Based Access
Learn how to add production-ready authentication to your Next.js 15 application using Auth.js v5. This comprehensive guide covers Google OAuth, email/password credentials, protected routes, middleware, and role-based access control.