Motion for React (Framer Motion) — Expert Skill
Motion (formerly Framer Motion, package: framer-motion OR motion) is the industry-standard animation library for React. It provides a hybrid engine: hardware-accelerated via Web Animations API with JavaScript fallback for springs, gestures, and layout.
References in this skill
| File | When to read it |
|---|---|
references/motion-values.md | useMotionValue, useTransform, useSpring, useVelocity, useMotionTemplate |
references/layout-animations.md | layout, layoutId, LayoutGroup, AnimatePresence, shared element transitions |
references/gestures-and-scroll.md | drag, whileHover/Tap/Focus, useScroll, scroll-linked animations |
references/patterns-and-performance.md | LazyMotion, MotionConfig, useReducedMotion, useAnimate, stagger, orchestration |
1. Import paths
// Standard React projects (framer-motion package)
import { motion, AnimatePresence, MotionConfig } from "framer-motion"
// motion package (newer naming, same API)
import { motion, AnimatePresence } from "motion/react"
// Next.js Server Components — use client-side import
import * as motion from "motion/react-client"
// Bundle optimization (replaces `motion` component)
import * as m from "motion/react-m"
Always check whether the project uses
framer-motionormotion. They share an identical API. Theframer-motionpackage is more common in existing codebases.
2. The motion component — foundation of everything
Any HTML or SVG element can be animated by prefixing with motion.:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.4, ease: "easeOut" }}
/>
Key animation props
| Prop | Purpose |
|---|---|
initial | Starting state (before mount or animation) |
animate | Target animated state |
exit | State when component is removed (requires AnimatePresence) |
transition | Controls timing, easing, type (tween/spring) |
style | Supports MotionValues as live style bindings |
variants | Named animation states defined externally |
whileHover | Animate while hovered |
whileTap | Animate while pressed |
whileFocus | Animate while focused |
whileDrag | Animate while dragging |
whileInView | Animate while in viewport |
layout | Auto-animate layout changes |
layoutId | Shared element transition between components |
Custom components
// React 19 — pass ref via props
const MyDiv = (props) => <div ref={props.ref} {...props} />
const MotionMyDiv = motion.create(MyDiv)
// React 18 — forwardRef pattern
const MyDiv = React.forwardRef((props, ref) => <div ref={ref} />)
const MotionMyDiv = motion.create(MyDiv)
3. Transitions
Transitions define how values move between states.
// Spring (default for physical/interactive feel)
transition={{ type: "spring", stiffness: 300, damping: 30 }}
// Tween (duration-based, predictable)
transition={{ type: "tween", duration: 0.5, ease: "easeInOut" }}
// Spring with visual duration (easier to reason about)
transition={{ type: "spring", visualDuration: 0.4, bounce: 0.25 }}
// Keyframe-specific timing
animate={{ x: [0, 100, 50, 150] }}
transition={{ times: [0, 0.3, 0.6, 1], duration: 1.5 }}
// Delay
transition={{ delay: 0.2, duration: 0.4 }}
// Per-property overrides
transition={{ duration: 1, x: { duration: 2, ease: "linear" } }}
Easing values
Standard: "linear", "easeIn", "easeOut", "easeInOut", "circIn", "circOut", "backIn", "backOut", "anticipate"
Custom cubic bezier: [0.17, 0.67, 0.83, 0.67]
4. Variants — declarative state machines for animations
Variants let you define named animation states and propagate them through the component tree automatically. They're the key to orchestrated, staggered animations across multiple elements.
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // delay between each child
delayChildren: 0.2, // delay before first child starts
when: "beforeChildren" // parent animates before children
}
}
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 300, damping: 24 }
}
}
// Children inherit "hidden"/"visible" state automatically
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.label}
</motion.li>
))}
</motion.ul>
Dynamic variants — pass per-element data via custom prop:
const variants = {
visible: (delay: number) => ({
opacity: 1,
transition: { delay }
})
}
<motion.li custom={index * 0.1} variants={variants} />
5. AnimatePresence — exit animations
Wrap components that conditionally render to enable exit animations:
import { AnimatePresence, motion } from "framer-motion"
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="modal" // key is required
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.2 }}
/>
)}
</AnimatePresence>
mode options
"sync"(default) — enter and exit simultaneously"wait"— exit finishes before enter starts"popLayout"— exiting element is removed from layout immediately (useful for lists)
List with exit animations
<AnimatePresence mode="popLayout">
{items.map(item => (
<motion.li
key={item.id}
layout // smooth reordering
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
/>
))}
</AnimatePresence>
6. whileX gesture props — quick reference
<motion.button
whileHover={{ scale: 1.05, boxShadow: "0 4px 20px rgba(0,0,0,0.15)" }}
whileTap={{ scale: 0.97 }}
whileFocus={{ outline: "2px solid #3b82f6" }}
transition={{ type: "spring", stiffness: 400, damping: 20 }}
/>
// Scroll-triggered
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
/>
For drag gestures and advanced gesture event handlers, read
references/gestures-and-scroll.md.
7. Layout animations — automatic FLIP
Add layout to any motion element and it smoothly animates whenever its size or position changes in the DOM.
// Accordion that animates open/close
function Accordion({ isOpen, children }) {
return (
<motion.div layout className="accordion">
{isOpen && <motion.div layout>{children}</motion.div>}
</motion.div>
)
}
// Shared element transition (magic move between components)
// Both components must have the same layoutId
function List({ items, selectedId, onSelect }) {
return items.map(item => (
<motion.li key={item.id} onClick={() => onSelect(item.id)}>
{selectedId === item.id && (
<motion.div layoutId="selected-indicator" className="underline" />
)}
{item.label}
</motion.li>
))
}
For
LayoutGroup,layoutScroll,layoutRoot, and complex shared transitions, readreferences/layout-animations.md.
8. Scroll-linked animations
import { useScroll, useTransform } from "framer-motion"
function ParallaxHero() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -200])
const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0])
return <motion.div style={{ y, opacity }}>Hero content</motion.div>
}
// Scoped to a specific element
function FadeInSection({ children }) {
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
})
const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0])
return <motion.div ref={ref} style={{ opacity }}>{children}</motion.div>
}
For
useInView,scroll()function,useVelocity, and advanced scroll patterns, readreferences/gestures-and-scroll.md.
9. Motion Values — reactive animation primitives
Motion values are observable values that drive animations without React re-renders. They're the performance-first approach for complex, interactive animations.
import { useMotionValue, useTransform, useSpring } from "framer-motion"
function MagneticButton() {
const x = useMotionValue(0)
const y = useMotionValue(0)
// Derive values — no re-renders
const rotateX = useTransform(y, [-50, 50], [15, -15])
const rotateY = useTransform(x, [-50, 50], [-15, 15])
// Spring-smoothed version of x
const smoothX = useSpring(x, { stiffness: 200, damping: 20 })
const handleMouseMove = (e: React.MouseEvent) => {
const rect = e.currentTarget.getBoundingClientRect()
x.set(e.clientX - rect.left - rect.width / 2)
y.set(e.clientY - rect.top - rect.height / 2)
}
return (
<motion.button
onMouseMove={handleMouseMove}
onMouseLeave={() => { x.set(0); y.set(0) }}
style={{ x: smoothX, rotateX, rotateY, transformPerspective: 500 }}
/>
)
}
For
useTransformwith multiple inputs,useMotionTemplate,useVelocity, anduseMotionValueEvent, readreferences/motion-values.md.
10. MotionConfig — global animation defaults
<MotionConfig
transition={{ type: "spring", stiffness: 300, damping: 30 }}
reducedMotion="user" // respects prefers-reduced-motion
>
{/* All child motion components use spring by default */}
<App />
</MotionConfig>
11. Common patterns cheat-sheet
Page transition (Next.js App Router)
// layout.tsx — wrap children
<AnimatePresence mode="wait">
<motion.main
key={pathname}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.main>
</AnimatePresence>
Staggered list reveal on mount
const list = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.08 } } }
const item = { hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0 } }
<motion.ul variants={list} initial="hidden" animate="visible">
{items.map(i => <motion.li key={i.id} variants={item}>{i.label}</motion.li>)}
</motion.ul>
Skeleton → content swap with layoutId
// Skeleton card
<motion.div layoutId={`card-${id}`} className="skeleton" />
// Loaded card (same layoutId, animates from skeleton position/size)
<motion.div layoutId={`card-${id}`} className="card">
{content}
</motion.div>
Drag-to-dismiss
<motion.div
drag="y"
dragConstraints={{ top: 0 }}
dragElastic={0.2}
onDragEnd={(_, info) => {
if (info.velocity.y > 500 || info.offset.y > 200) onDismiss()
}}
exit={{ y: "100%", opacity: 0 }}
/>
12. Quick diagnostics
| Symptom | Likely cause & fix |
|---|---|
| Exit animation not playing | Missing AnimatePresence wrapper or missing key prop |
| Layout animation jumpy | Wrap siblings in LayoutGroup; add layoutScroll to scrollable parents |
motion.create() breaks animations | Called inside render — move to module scope |
| High re-render count during animation | Use useMotionValue + style prop instead of React state |
whileInView fires on every render | Add viewport={{ once: true }} |
| Spring feels wrong | Use visualDuration + bounce instead of stiffness/damping — easier to tune |