Framer Motion is the most capable React animation library. But most developers use it for simple fade-ins when it's capable of orchestrated sequences, shared layout animations, and gesture-driven interactions that feel native.
Core Concepts
Framer Motion works on a simple model: describe the target state, and the library figures out the transition.
import { motion } from 'framer-motion'
// Basic animation
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
/>
// Viewport-triggered animation
<motion.div
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5 }}
/>
Variants for Orchestrated Sequences
Variants let parent components coordinate children:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // each child delays by 100ms
delayChildren: 0.2,
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
function ProductGrid({ products }) {
return (
<motion.div
variants={containerVariants}
initial='hidden'
animate='visible'
className='grid grid-cols-3 gap-6'
>
{products.map(product => (
<motion.div key={product.id} variants={itemVariants}>
<ProductCard product={product} />
</motion.div>
))}
</motion.div>
)
}
Each card fades up one after another — zero per-item configuration needed.
Layout Animations
Animate layout changes automatically:
// List that reorders smoothly
function SortableList({ items }) {
return (
<motion.ul layout>
{items.map(item => (
<motion.li
key={item.id}
layout // animates position changes
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{item.name}
</motion.li>
))}
</motion.ul>
)
}
Add layout to any motion element and Framer Motion smoothly transitions between positions when items reorder.
Shared Layout with layoutId
Animate elements across different DOM positions:
// Tab indicator that slides between tabs
function Tabs({ tabs, activeTab, onSelect }) {
return (
<div className='flex gap-4 border-b'>
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => onSelect(tab.id)}
className='relative pb-2'
>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId='tab-indicator' // same ID = shared animation
className='absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500'
/>
)}
</button>
))}
</div>
)
}
Gesture Animations
// Draggable card
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
dragElastic={0.1}
whileDrag={{ scale: 1.05, rotate: 3 }}
/>
// Button hover + tap
<motion.button
whileHover={{ scale: 1.02, y: -1 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
/>
Page Transitions
// app/layout.tsx -- wrap pages for transitions
import { AnimatePresence } from 'framer-motion'
export default function Layout({ children }) {
return (
<AnimatePresence mode='wait'>
{children}
</AnimatePresence>
)
}
// Each page
<motion.main
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
>
{content}
</motion.main>
Performance
Framer Motion uses the Web Animations API and CSS transforms — GPU-accelerated. For performance-critical animations:
- Animate
transformandopacityonly (avoids layout) - Use
will-change: transformon frequently animated elements - Use
useReducedMotion()to respect user preferences
import { useReducedMotion } from 'framer-motion'
function AnimatedCard({ children }) {
const prefersReduced = useReducedMotion()
return (
<motion.div
animate={{ y: prefersReduced ? 0 : -4 }}
whileHover={{ y: prefersReduced ? 0 : -8 }}
>
{children}
</motion.div>
)
}
The AI SaaS Starter at whoffagents.com ships with Framer Motion configured: page transitions, staggered card grids, and hover animations on CTAs — all with useReducedMotion support. $99 one-time.
Top comments (0)