User interfaces feel alive when they respond to interaction with smooth, intentional motion. A button that scales on press, a modal that fades in, a list that staggers its children into view—these animations communicate state changes, guide attention, and create a sense of direct manipulation. Without a dedicated animation library, achieving this in React means wrestling with CSS transitions, requestAnimationFrame, refs to track DOM elements, and cleanup logic that’s easy to get wrong. The result is brittle animation code scattered across components.
Framer Motion solves this by providing a declarative animation API built specifically for React. Instead of imperatively managing keyframes and timers, you describe the desired state of an element—animate={{ opacity: 1, y: 0 }}—and the library handles interpolation, spring physics, and DOM updates. The motion component wrapper replaces standard HTML elements (motion.div, motion.button) and adds animation props that integrate naturally with React’s component model, including automatic cleanup on unmount.
Production web projects use Framer Motion extensively for page transitions, interactive hover effects on feature cards, staggered list animations in documentation sidebars, and mount/unmount transitions via AnimatePresence. The library’s layout animation system powers smooth reordering in dashboard widgets, while custom spring configurations ensure consistent motion design across the product.
Every HTML and SVG element has a motion counterpart: motion.div, motion.span, motion.svg, etc. These components accept all standard props plus animation-specific props like animate, initial, exit, transition, whileHover, and whileTap. Under the hood, motion components use a performant animation engine that batches DOM writes and uses hardware-accelerated transforms wherever possible.
Variants are named animation states defined as objects. A parent component can propagate variant changes to all children automatically, enabling orchestrated animations like staggered list reveals. The parent defines transition.staggerChildren, and each child animates in sequence without any explicit coordination logic. Variants decouple animation definitions from component rendering, making them reusable and composable.
React removes DOM nodes immediately on unmount—there is no built-in way to animate an element out before removal. AnimatePresence wraps conditionally rendered children and delays their removal until the exit animation completes. This is essential for modals, toasts, route transitions, and any UI where elements leave the screen gracefully rather than vanishing.
When an element’s position or size changes due to a re-render (e.g., items reordering in a list, a panel expanding), adding the layout prop to a motion component makes it automatically animate between its old and new positions using FLIP (First, Last, Invert, Play). This avoids manual measurement and transforms—Framer Motion calculates the difference and animates it with a single prop.
Framer Motion defaults to spring-based animations rather than duration-based easing curves. Springs feel more natural because they model physical momentum and settling. You configure springs with stiffness, damping, and mass, or use presets like type: "spring", bounce: 0.3. For precise timing control, tween transitions with cubic-bezier easing are also supported.
The simplest usage replaces a <div> with motion.div and adds initial and animate props. The component automatically transitions between these states on mount.
import { motion } from "framer-motion";
function FadeInCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="card"
>
{children}
</motion.div>
);
}
Variants define animation states by name. A parent variant with staggerChildren propagates to children automatically, creating staggered reveal effects without manual delay calculations.
import { motion } from "framer-motion";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: {
opacity: 1,
x: 0,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
function FeatureList({ features }: { features: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{features.map((feature) => (
<motion.li key={feature} variants={itemVariants}>
{feature}
</motion.li>
))}
</motion.ul>
);
}
AnimatePresence wraps conditionally rendered motion components and runs their exit animation before removing them from the DOM. The mode prop controls whether entering and exiting elements animate simultaneously or sequentially.
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
function Modal({ isOpen, onClose, children }: {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
return (
<AnimatePresence mode="wait">
{isOpen && (
<motion.div
key="modal-backdrop"
className="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
>
<motion.div
key="modal-content"
className="modal"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -10 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
onClick={(e) => e.stopPropagation()}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
The layout prop enables automatic FLIP animations when an element’s position changes across re-renders. Combined with AnimatePresence, this handles both reordering and removal of items in a list.
import { motion, AnimatePresence } from "framer-motion";
interface Task {
id: string;
title: string;
completed: boolean;
}
function TaskList({ tasks }: { tasks: Task[] }) {
return (
<AnimatePresence>
{tasks.map((task) => (
<motion.div
key={task.id}
layout
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{
layout: { type: "spring", stiffness: 400, damping: 30 },
opacity: { duration: 0.2 },
}}
className={`task ${task.completed ? "completed" : ""}`}
>
{task.title}
</motion.div>
))}
</AnimatePresence>
);
}
Framer Motion provides gesture handlers that trigger animation states. whileHover, whileTap, whileFocus, and whileDrag apply animation variants for the duration of the gesture, reverting when the gesture ends.
import { motion } from "framer-motion";
function InteractiveButton({ label, onClick }: {
label: string;
onClick: () => void;
}) {
return (
<motion.button
onClick={onClick}
whileHover={{ scale: 1.05, boxShadow: "0 8px 30px rgba(108,99,255,0.3)" }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="cta-button"
>
{label}
</motion.button>
);
}
For instance, a product landing page can use Framer Motion to power the hero section with staggered text reveals, parallax scroll effects on feature sections, and smooth tab transitions in the product showcase.
Each feature card uses whileHover to elevate on mouse-over with a spring animation that overshoots slightly before settling, creating a satisfying tactile feel.
The website project uses AnimatePresence with Next.js App Router to animate route transitions. A layout component wraps page content in a motion component keyed by the route path, fading and sliding pages in and out as the user navigates. The sidebar navigation uses layout animations so that the active indicator bar slides smoothly between items rather than jumping.
Dashboard components in the internal tooling use layout animations for widget reordering. Users drag-and-drop dashboard cards, and the remaining cards animate to fill the gap. The Reorder component from Framer Motion handles the drag detection, reorder logic, and FLIP animation in a single composable API, eliminating the need for a separate drag-and-drop library for simple reordering use cases.
key prop with AnimatePresence: AnimatePresence uses the key prop to track which children are entering and exiting. If elements lack unique keys, exit animations will not fire because React cannot distinguish removed elements from re-rendered ones. Always use stable, unique keys on direct children of AnimatePresence.width, height, top, or left triggers layout recalculations on every frame. Prefer x, y, scale, rotate, and opacity—these are GPU-accelerated and skip layout. Use layout animations if you need actual size changes to animate smoothly.AnimatePresence exit completion and can cause stacking elements. Use damping values of 15–30 for most UI animations, or set restDelta and restSpeed thresholds to force early settling.initial. Memoize parent components or lift state to prevent unnecessary re-renders near animated elements.layoutDependency or split the animation into separate phases—layout first, then content fade.motion/mini (the tree-shakeable subset) or CSS animations with transition properties instead of importing the full library.