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)
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