TL;DR: Framer Motion is the go-to animation library for React. Wrap any element in
<motion.div>, passanimateprops, and get spring-physics-driven transitions.AnimatePresencehandles exit animations that plain CSS can't. At ~97kB it's not tiny, but it's worth it. Official docs: framer.com/motion.
What is Framer Motion?
Framer Motion is a declarative animation library for React that covers the full spectrum from micro-interactions to complex orchestrated sequences. Its spring physics engine produces animations that feel natural rather than mechanical, and its AnimatePresence component solves one of React's hardest problems: animating elements as they leave the DOM.
I used to fake exit animations with a setTimeout and a visible boolean, delaying the actual unmount just long enough for a CSS transition to play. It worked, mostly, until a fast click sequence left ghost elements stuck mid-fade. Swapping that whole pattern for AnimatePresence took about twenty minutes and deleted three separate timeout hacks scattered across the codebase.
When to use it
Use Framer Motion for:
- Exit animations - CSS can't animate elements being removed from the DOM; Framer can
- Layout animations - automatically animate position/size changes when items reorder
- Gesture-driven interactions - drag, swipe, hover with spring physics
- Complex orchestration - stagger, sequence, and synchronize multiple animations
For simple hover/focus effects, CSS transitions are faster and have zero runtime cost. Framer Motion's ~97kB (gzipped ~30kB) is worth it only when you need what CSS can't do.
Key Features
motion.*components - drop-in replacements for any HTML/SVG elementAnimatePresence- enables exit animations for unmounted components- The
layoutprop - animates position/size changes automatically - Variants - named animation states for orchestrating complex sequences
- Gestures -
whileHover,whileTap,whileDragwith spring physics - Scroll animations -
useScroll,useTransformfor scroll-linked effects
Installation
npm install framer-motion
Basic Usage
import { motion, AnimatePresence } from 'framer-motion'
// Simple enter animation
function FadeIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
)
}
// Exit animation with AnimatePresence
function Toast({ show, message }: { show: boolean; message: string }) {
return (
<AnimatePresence>
{show && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
{message}
</motion.div>
)}
</AnimatePresence>
)
}
Stagger Children with Variants
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
}
const item = {
hidden: { opacity: 0, x: -20 },
show: { opacity: 1, x: 0 },
}
function AnimatedList({ items }: { items: string[] }) {
return (
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((text) => (
<motion.li key={text} variants={item}>
{text}
</motion.li>
))}
</motion.ul>
)
}
Scroll-linked Animations
import { useScroll, useTransform, motion } from 'framer-motion'
function ParallaxHero() {
const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 500], [0, -150])
return (
<motion.div style={{ y }}>
<img src="/hero.jpg" alt="Hero" />
</motion.div>
)
}
Draggable Card with Gesture Constraints
drag plus dragConstraints is the pattern I reach for on swipe-to-dismiss cards and reorderable lists where a full drag-and-drop library would be overkill:
import { motion, useMotionValue, useTransform } from 'framer-motion'
function SwipeToDismissCard({ onDismiss }: { onDismiss: () => void }) {
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
return (
<motion.div
style={{ x, opacity }}
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.7}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 150) onDismiss()
}}
>
Swipe me away
</motion.div>
)
}
dragElastic controls how far the card can travel past the constraint before snapping back, and onDragEnd reads the final offset to decide whether the swipe counted as a dismissal or just a nudge. No separate touch-event listeners, no manual velocity math.
TypeScript Tips
Use Variants type for typed animation states:
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 },
},
}
Common Gotchas
AnimatePresenceneeds a key - the child must have a stablekeyprop for exit animations to trigger correctly- Layout animations and
overflow: hidden- parent elements withoverflow: hiddenclip layout animations; addlayoutto the parent too or uselayoutId - Bundle size - if you only need scroll animations, import from
framer-motion/clientor use the new Motion One alternative initial={false}- pass toAnimatePresenceto skip enter animations on first render (important for SSR)
What It Costs You
Framer Motion is the most capable animation library in the React ecosystem and also one of the heavier dependencies you'll add for something users can turn off. Worth weighing before it goes in.
The bundle is the obvious cost. A fade-in on scroll and a hover scale do not need a full animation runtime; the CSS equivalents ship for nothing and run off the main thread. Reach for this when you need orchestration that CSS genuinely can't express: shared layout transitions between routes, gesture-driven drags with momentum, exit animations that must finish before an element unmounts. Those are real gaps, and nothing else fills them as well.
The cost people forget is accessibility. Layout animations move content while someone is reading it, and for a reader with a vestibular disorder that isn't a flourish, it's a symptom. Wrap the app in MotionConfig with reducedMotion="user" so every animation respects the OS setting by default, rather than remembering a check at each call site. You'll forget one otherwise, and the one you forget will be the page transition.
Last, animate transform and opacity and leave the rest alone. Animating width, height, or top forces layout on every frame, and no library makes that cheap. The layout prop exists precisely so you can get the visual result of a size change through a transform instead.
Related
- React 19 Guide -
useOptimisticproduces pending states you can animate smoothly with Framer Motion's layout animations - Vaul - drawer component with built-in gesture animations; Framer Motion for more custom animation control
- dnd kit - sortable drag-and-drop; combine with Framer Motion
layoutprop for animated reordering