Motion for React (formerly Framer Motion) is a declarative animation library that handles fade-ins, scroll-triggered effects, exit animations, and layout transitions through simple props like initial, animate, and exit — all while staying in sync with React state.
Before this library existed, I spent too much time wrestling with @keyframes and CSS transitions. Interactive animations — hover effects, modals, page transitions — got especially messy when mixed with React state management. Motion changed that entirely. It's declarative, works naturally with React state, and handles the tricky parts (like animating components as they unmount) automatically. This article covers practical patterns from basic fade-ins to scroll-triggered effects and layout animations.
Core Concepts
Motion centers around the motion component. Prefix any HTML element with motion. and it gains animation capabilities:
import { motion } from "motion/react";
function FadeInBox() {
return (
<motion.div
initial={{ opacity: 0 }} // Starting state
animate={{ opacity: 1 }} // Target state
transition={{ duration: 0.5 }} // Animation settings
>
This fades in on mount
</motion.div>
);
}
Three props to learn:
initial— state before the animation startsanimate— target state to animate totransition— duration, easing, delay, etc.
Common Animation Properties
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
duration: 0.4,
ease: "easeOut",
delay: 0.1,
}}
>
Content
</motion.div>
Almost any CSS property works, but prefer opacity and transform properties (x, y, scale, rotate). They're GPU-accelerated and don't trigger layout reflow — keeping your animations smooth.
Fade and Slide Patterns
Starting with the most common patterns.
Page Load Fade-in
"use client";
import { motion } from "motion/react";
function HeroSection() {
return (
<section>
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
Page Title
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.15 }}
>
Subtext here
</motion.p>
<motion.button
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut", delay: 0.3 }}
>
Get Started
</motion.button>
</section>
);
}
Incrementing delay creates a stagger effect — elements appear sequentially.
Hover Animations
"use client";
import { motion } from "motion/react";
function AnimatedCard({ title, description }: { title: string; description: string }) {
return (
<motion.div
className="rounded-lg border p-6 cursor-pointer"
whileHover={{
scale: 1.02,
boxShadow: "0 10px 40px rgba(0,0,0,0.15)",
}}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
<h3>{title}</h3>
<p>{description}</p>
</motion.div>
);
}
whileHover and whileTap are the declarative equivalents of CSS :hover and :active.
Variants for Complex Animations
When multiple elements animate together, use variants — named animation states that can be shared between parent and children.
"use client";
import { motion } from "motion/react";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Stagger children by 0.1s
delayChildren: 0.2, // Wait 0.2s before starting children
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeOut" },
},
};
function AnimatedList({ items }: { items: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item, i) => (
<motion.li key={i} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}
staggerChildren does the heavy lifting here. When the container transitions to "visible", each child starts its own animation with a delay offset. No manual delay management needed — just pass the same variants to each motion.li.
Scroll-Triggered Animations (whileInView)
whileInView triggers an animation when the element enters the viewport:
"use client";
import { motion } from "motion/react";
function FeatureCard({ icon, title, description }: FeatureCardProps) {
return (
<motion.div
className="rounded-xl p-8 border"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }} // Trigger 50px before fully in view
transition={{ duration: 0.5, ease: "easeOut" }}
>
<div className="text-4xl mb-4">{icon}</div>
<h3 className="text-xl font-bold mb-2">{title}</h3>
<p>{description}</p>
</motion.div>
);
}
viewport options:
once: true— animate only the first time it enters view (recommended for content reveals)margin— offset from the viewport edge; negative values trigger before fully visible
AnimatePresence: Animate Mount and Unmount
By default, React removes components from the DOM instantly. AnimatePresence holds them in the DOM until their exit animation completes.
"use client";
import { motion, AnimatePresence } from "motion/react";
function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
key="overlay"
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div
key="modal"
className="fixed inset-x-4 top-1/2 -translate-y-1/2 max-w-lg mx-auto
bg-white rounded-2xl p-8 shadow-2xl z-10"
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", stiffness: 350, damping: 30 }}
>
{children}
<button onClick={onClose}>Close</button>
</motion.div>
</>
)}
</AnimatePresence>
);
}
The exit prop defines the animation that plays before the component is removed. Without AnimatePresence, exit is ignored.
Toast Notifications
"use client";
import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";
type Toast = { id: number; message: string };
function ToastContainer() {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = (message: string) => {
const id = Date.now();
setToasts(prev => [...prev, { id, message }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
};
return (
<>
<button onClick={() => addToast("Saved!")}>Save</button>
<div className="fixed bottom-4 right-4 flex flex-col gap-2">
<AnimatePresence>
{toasts.map(toast => (
<motion.div
key={toast.id}
className="bg-gray-900 text-white px-4 py-3 rounded-lg shadow-lg"
initial={{ opacity: 0, x: 50, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 50, scale: 0.95 }}
transition={{ duration: 0.2 }}
>
{toast.message}
</motion.div>
))}
</AnimatePresence>
</div>
</>
);
}
useAnimate for Imperative Sequences
For complex sequential animations or animations triggered at specific moments, useAnimate gives you imperative control:
"use client";
import { useAnimate } from "motion/react";
function SubmitButton() {
const [scope, animate] = useAnimate();
const handleSubmit = async () => {
// 1. Press feedback
await animate(scope.current, { scale: 0.95 }, { duration: 0.1 });
await animate(scope.current, { scale: 1 }, { duration: 0.1 });
// 2. Loading state
animate(scope.current, { opacity: 0.7 });
try {
await submitForm();
// 3. Success feedback
await animate(scope.current, { backgroundColor: "#22c55e" }, { duration: 0.3 });
await animate(scope.current, { backgroundColor: "#3b82f6" }, { duration: 0.5 });
} catch {
// 4. Error shake
await animate(scope.current, { x: [-8, 8, -8, 8, 0] }, { duration: 0.4 });
}
};
return (
<button ref={scope} onClick={handleSubmit} className="px-6 py-3 bg-blue-500 rounded-lg">
Submit
</button>
);
}
animate returns a Promise, so you can await each step and chain them sequentially.
Layout Animations with layoutId
layoutId smoothly animates an element from one position to another — even across different component renders:
"use client";
import { motion, LayoutGroup } from "motion/react";
import { useState } from "react";
function TabNav() {
const [activeTab, setActiveTab] = useState("home");
const tabs = ["home", "about", "contact"];
return (
<LayoutGroup>
<nav className="flex gap-4 relative">
{tabs.map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className="relative px-4 py-2"
>
{tab}
{activeTab === tab && (
<motion.div
layoutId="active-indicator"
className="absolute inset-0 bg-blue-100 rounded-md -z-10"
/>
)}
</button>
))}
</nav>
</LayoutGroup>
);
}
When active-indicator moves to a different tab, Framer Motion automatically animates it from the old position to the new one. No manual coordinate tracking needed.
Performance Best Practices
Stick to Transform and Opacity
// ❌ Causes layout reflow — expensive
<motion.div animate={{ width: "100%", height: 200, top: 50 }} />
// ✅ GPU-accelerated — no layout reflow
<motion.div animate={{ scaleX: 1, scaleY: 1, y: 50, opacity: 1 }} />
When you animate properties like width, height, or top, the browser recalculates layout for every frame. Sticking to transform and opacity keeps animations in the GPU and off the main thread.
Reduce Motion for Accessibility
"use client";
import { motion, useReducedMotion } from "motion/react";
function AnimatedCard({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.4 }}
>
{children}
</motion.div>
);
}
useReducedMotion respects the user's OS-level "Reduce Motion" preference (the prefers-reduced-motion media query). Always consider this for production apps.
FAQ
What's the difference between Framer Motion and Motion?
Motion is the new name for Framer Motion. In 2025, the library became an independent project under the motion package on npm. The API is identical — you only need to change your import from "framer-motion" to "motion/react". The old framer-motion package still installs but is no longer actively developed.
Do I need "use client" for every animated component in Next.js?
Yes. Any file that imports from motion/react and uses motion components needs the "use client" directive in Next.js App Router. The best practice is to keep your page-level components as Server Components and extract only the animated sections into small Client Components.
How do I animate a component when it unmounts?
Wrap it with AnimatePresence and add an exit prop. Without AnimatePresence, React removes the component instantly from the DOM and exit is ignored.
Is Motion heavy? How much does it add to my bundle?
The core of Motion is around 32 KB gzipped (tree-shakeable). If you only use basic animations (motion.div with initial/animate), the actual shipped code is smaller thanks to tree-shaking. For comparison, a full CSS animation library like GSAP is similar in size but doesn't integrate with React state.
Can I use Motion with React Server Components directly?
No. Motion components are client-side by nature — they require browser APIs and React state. Use them inside "use client" components. Server Components handle data fetching and static rendering; Client Components handle interactivity and animation.
What's the best way to stagger a list of items?
Use variants with staggerChildren on the parent container. This is more maintainable than manually setting delay on each item. A staggerChildren value of 0.05–0.1 seconds feels natural for most lists.
How do I handle prefers-reduced-motion?
Use the useReducedMotion hook. It returns true when the user has enabled "Reduce Motion" in their OS settings. Set duration: 0 or skip transform animations accordingly — keep opacity changes for content visibility.
Wrapping Up
Core Motion patterns in one table:
| Pattern | Use Case | Key Props |
|---|---|---|
| Basic animation | Fade-in, slide | initial, animate, transition |
| Hover / tap | Interactive feedback | whileHover, whileTap |
| Variants | List stagger, coordinated animations | variants, staggerChildren |
| Scroll-triggered | Content reveal | whileInView, viewport |
| Mount / unmount | Modals, toasts | AnimatePresence, exit |
| Sequential control | Complex choreography | useAnimate |
| Layout changes | Tabs, list reordering | layoutId, LayoutGroup |
The most important rule: don't over-animate. opacity + y covers 80% of cases beautifully. Start simple, reach for complex patterns only when needed, and always keep transform/opacity as your primary animation properties for performance. Check the Motion documentation and changelog for the latest features and updates.
Related articles: