Framer Motion Complete Guide | React Animations Made Simple
이 글의 핵심
Framer Motion is the go-to animation library for React — declarative, GPU-accelerated, and powerful enough for complex page transitions and gesture-driven UIs. This guide covers everything from basic animations to advanced layout animations.
What This Guide Covers
Framer Motion makes React animations declarative and composable. Drop in a motion component, add animate props, and it handles the rest — spring physics, gesture detection, and smooth layout transitions.
Real-world insight: Replacing CSS transitions with Framer Motion variants cut animation code by 60% and finally made exit animations (modal close, toast dismiss) work correctly without hacks.
Installation
npm install framer-motion
1. Basic Animation
Replace any HTML element with its motion equivalent:
import { motion } from 'framer-motion'
// Animate on mount
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
Hello World
</motion.div>
// Any HTML element
<motion.h1 animate={{ scale: 1.1 }} />
<motion.button whileHover={{ scale: 1.05 }} />
<motion.img animate={{ rotate: 360 }} />
2. Transition Configuration
// Spring physics (natural feel)
<motion.div
animate={{ x: 100 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
/>
// Tween (controlled timing)
<motion.div
animate={{ opacity: 1 }}
transition={{ type: 'tween', duration: 0.3, ease: 'easeOut' }}
/>
// Delay and repeat
<motion.div
animate={{ y: [0, -10, 0] }} // keyframes
transition={{ repeat: Infinity, duration: 1, ease: 'easeInOut' }}
/>
// Stagger children (via parent)
<motion.ul
initial="hidden"
animate="visible"
variants={{
visible: { transition: { staggerChildren: 0.1 } }
}}
>
{items.map(item => (
<motion.li
key={item.id}
variants={{
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 }
}}
/>
))}
</motion.ul>
3. Variants
Variants define named animation states and let you propagate animation to children:
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: 'easeOut' }
},
hover: { scale: 1.02, boxShadow: '0 10px 30px rgba(0,0,0,0.1)' },
tap: { scale: 0.98 },
}
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
whileTap="tap"
>
<h2>Card Title</h2>
</motion.div>
4. Gesture Animations
// Hover and tap
<motion.button
whileHover={{ scale: 1.05, backgroundColor: '#3b82f6' }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
Click me
</motion.button>
// Drag
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
dragElastic={0.2}
whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
style={{ cursor: 'grab', width: 100, height: 100, background: '#3b82f6' }}
/>
// Drag with snap back
<motion.div
drag
dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
// dragConstraints = same as origin → snaps back
/>
5. AnimatePresence (Exit Animations)
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
function Modal({ isOpen, onClose }) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)' }}
/>
{/* Modal */}
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: 'spring', duration: 0.3 }}
style={{ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
>
<p>Modal content</p>
<button onClick={onClose}>Close</button>
</motion.div>
</>
)}
</AnimatePresence>
)
}
// Toast notifications
function ToastList({ toasts }) {
return (
<div style={{ position: 'fixed', bottom: 20, right: 20 }}>
<AnimatePresence>
{toasts.map(toast => (
<motion.div
key={toast.id}
initial={{ opacity: 0, x: 100, scale: 0.9 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 100 }}
layout // smooth reorder when toasts are added/removed
>
{toast.message}
</motion.div>
))}
</AnimatePresence>
</div>
)
}
6. Page Transitions (Next.js)
// components/PageTransition.jsx
import { motion, AnimatePresence } from 'framer-motion'
import { useRouter } from 'next/router'
const pageVariants = {
initial: { opacity: 0, x: -20 },
animate: { opacity: 1, x: 0 },
exit: { opacity: 0, x: 20 },
}
export function PageTransition({ children }) {
const { pathname } = useRouter()
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
</AnimatePresence>
)
}
7. Layout Animations
Automatically animate size and position changes — no manual calculations:
import { motion, LayoutGroup } from 'framer-motion'
import { useState } from 'react'
function Accordion() {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.div layout onClick={() => setIsOpen(!isOpen)} style={{ overflow: 'hidden' }}>
<motion.h3 layout>Click to expand</motion.h3>
{isOpen && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Hidden content revealed with smooth height animation
</motion.p>
)}
</motion.div>
)
}
// Shared layout (element morphs between positions)
function TabList() {
const [activeTab, setActiveTab] = useState('home')
const tabs = ['home', 'about', 'contact']
return (
<LayoutGroup>
<div style={{ display: 'flex', gap: 8 }}>
{tabs.map(tab => (
<button key={tab} onClick={() => setActiveTab(tab)} style={{ position: 'relative' }}>
{tab}
{activeTab === tab && (
<motion.div
layoutId="active-tab" // same layoutId = shared layout animation
style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'blue' }}
/>
)}
</button>
))}
</div>
</LayoutGroup>
)
}
8. useAnimation Hook
Control animations imperatively:
import { motion, useAnimation } from 'framer-motion'
import { useEffect } from 'react'
function ShakeInput({ hasError }) {
const controls = useAnimation()
useEffect(() => {
if (hasError) {
controls.start({
x: [0, -10, 10, -10, 10, 0],
transition: { duration: 0.4 }
})
}
}, [hasError])
return (
<motion.input
animate={controls}
style={{ borderColor: hasError ? 'red' : 'gray' }}
/>
)
}
9. Scroll Animations
import { motion, useScroll, useTransform } from 'framer-motion'
import { useRef } from 'react'
// Scroll progress
function HeroSection() {
const { scrollY } = useScroll()
const opacity = useTransform(scrollY, [0, 300], [1, 0])
const y = useTransform(scrollY, [0, 300], [0, -100])
return (
<motion.section style={{ opacity, y }}>
<h1>Parallax Hero</h1>
</motion.section>
)
}
// Animate when element enters viewport
function FadeInSection({ children }) {
const ref = useRef(null)
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
)
}
Key Takeaways
| Concept | Use for |
|---|---|
initial / animate | Mount animations |
exit + AnimatePresence | Unmount animations (modals, toasts) |
whileHover / whileTap | Gesture feedback |
variants | Named states, propagate to children, stagger |
layout | Automatic size/position change animation |
layoutId | Shared layout — element morphs between components |
whileInView | Animate when element enters viewport |
useScroll + useTransform | Scroll-driven animations |
Framer Motion’s declarative API makes animations feel like a design system — define states (hidden, visible, hover) and let the library handle interpolation. Start with initial/animate/exit, add variants for reuse, and reach for layout and layoutId when you need elements to animate smoothly between states.