Introduction
Framer Motion (now simply called Motion) has become the de facto standard for animations in React applications. Built on top of the Web Animations API, it provides a declarative, physics-based animation system that makes complex animations feel natural and effortless. What sets Motion apart from other animation libraries is its deep integration with React's component model, layout animation capabilities that handle shared element transitions automatically, and an AnimatePresence component that elegantly handles mounting and unmounting animations.
The library was originally created by Framer for their website builder tool but quickly gained popularity in the broader React community. Its API is designed around the principle that animations should be declarative — you describe what you want, and Motion figures out how to get there. This means you rarely need to manage animation state manually, write complex timeline code, or worry about interrupted animations.
In this comprehensive guide, we'll explore every aspect of Motion, from basic motion components and gesture animations to advanced layout animations and page transitions. We'll build real-world examples including animated modals, drag-and-drop interfaces, shared layout transitions, and staggered list animations. By the end, you'll have the knowledge to create polished, performant animations that elevate your React applications.
Understanding Motion: Core Concepts
The motion Component
Motion provides motion versions of every HTML and SVG element. These components accept animation props that control their visual behavior:
import { motion } from 'framer-motion';
// Basic motion component
function AnimatedBox() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
I'm animated!
</motion.div>
);
}
// SVG animation
function AnimatedIcon() {
return (
<motion.svg
width="50"
height="50"
viewBox="0 0 50 50"
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
>
<motion.circle
cx="25"
cy="25"
r="20"
fill="none"
stroke="#005fcc"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.5, ease: 'easeInOut' }}
/>
</motion.svg>
);
}Animation Props
interface MotionProps {
// Initial state (before animation)
initial?: Target | boolean;
// Target state to animate to
animate?: Target;
// State when component exits (requires AnimatePresence)
exit?: Target;
// While hovering
whileHover?: Target;
// While pressing/clicking
whileTap?: Target;
// While dragging
whileDrag?: Target;
// While element has focus
whileFocus?: Target;
// While element is in viewport
whileInView?: Target;
// Animation transition configuration
transition?: Transition;
// Drag constraints and configuration
drag?: boolean | 'x' | 'y';
dragConstraints?: { top?: number; bottom?: number; left?: number; right?: number } | RefObject;
dragElastic?: number;
dragMomentum?: boolean;
// Layout animation (automatic position/size animation)
layout?: boolean | string;
layoutId?: string;
// Variants (named animation states)
variants?: Variants;
// Controls which variant to show
custom?: any;
}Transition Types
// Spring physics (default)
<motion.div
animate={{ x: 100 }}
transition={{
type: 'spring',
stiffness: 300, // Higher = faster
damping: 20, // Higher = less bouncy
mass: 1, // Higher = slower, heavier
}}
/>
// Tween (duration-based)
<motion.div
animate={{ opacity: 1 }}
transition={{
type: 'tween',
duration: 0.5,
ease: 'easeInOut', // or cubic-bezier array
}}
/>
// Inertia (deceleration-based)
<motion.div
animate={{ x: 500 }}
transition={{
type: 'inertia',
velocity: 200,
power: 0.8,
timeConstant: 300,
}}
/>
// Keyframes (multiple values)
<motion.div
animate={{ x: [0, 100, 50, 100] }}
transition={{
duration: 1,
times: [0, 0.3, 0.6, 1], // When each keyframe occurs
}}
/>Gesture Animations
Hover and Tap
function InteractiveButton() {
return (
<motion.button
whileHover={{
scale: 1.05,
boxShadow: '0 10px 20px rgba(0,0,0,0.1)',
}}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
style={{
padding: '12px 24px',
fontSize: '16px',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
}}
>
Click me
</motion.button>
);
}
// Magnetic hover effect
function MagneticButton({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLButtonElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouse = (e: React.MouseEvent) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
setPosition({
x: (e.clientX - centerX) * 0.2,
y: (e.clientY - centerY) * 0.2,
});
};
const resetMouse = () => setPosition({ x: 0, y: 0 });
return (
<motion.button
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={resetMouse}
animate={{ x: position.x, y: position.y }}
whileHover={{ scale: 1.1 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
>
{children}
</motion.button>
);
}Drag Interactions
import { motion, useMotionValue, useTransform } from 'framer-motion';
function DraggableCard() {
const x = useMotionValue(0);
const y = useMotionValue(0);
// Transform drag distance to rotation
const rotateX = useTransform(y, [-100, 100], [10, -10]);
const rotateY = useTransform(x, [-100, 100], [-10, 10]);
return (
<motion.div
drag
dragConstraints={{ top: -100, bottom: 100, left: -100, right: 100 }}
dragElastic={0.1}
whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
style={{ x, y, rotateX, rotateY, cursor: 'grab' }}
className="card"
>
Drag me around
</motion.div>
);
}
// Swipe to dismiss
function SwipeCard({ onDismiss }: { onDismiss: () => void }) {
const x = useMotionValue(0);
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.5}
style={{ x, opacity }}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 100) {
onDismiss();
}
}}
whileDrag={{ scale: 0.95 }}
>
<div className="card-content">Swipe to dismiss</div>
</motion.div>
);
}AnimatePresence: Mount/Unmount Animations
Basic Usage
import { AnimatePresence, motion } from 'framer-motion';
function ToggleContent() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="content"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
Content goes here
</motion.div>
)}
</AnimatePresence>
</div>
);
}Modal with Backdrop
function AnimatedModal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
className="modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
>
<motion.div
className="modal-content"
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', damping: 25, stiffness: 300 }}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
>
{children}
<button onClick={onClose}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}Animated List with Add/Remove
import { AnimatePresence, motion } from 'framer-motion';
interface Item {
id: string;
text: string;
}
function AnimatedList() {
const [items, setItems] = useState<Item[]>([]);
const addItem = () => {
setItems(prev => [
...prev,
{ id: Date.now().toString(), text: `Item ${prev.length + 1}` },
]);
};
const removeItem = (id: string) => {
setItems(prev => prev.filter(item => item.id !== id));
};
return (
<div>
<button onClick={addItem}>Add Item</button>
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100, transition: { duration: 0.2 } }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
style={{
padding: '12px',
margin: '8px 0',
background: '#f0f0f0',
borderRadius: '8px',
}}
>
<span>{item.text}</span>
<button onClick={() => removeItem(item.id)}>×</button>
</motion.div>
))}
</AnimatePresence>
</div>
);
}Layout Animations
Automatic Layout Transitions
// Automatic layout animation - Motion handles position/size changes
function ExpandableCard() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
style={{
background: 'white',
borderRadius: '12px',
padding: '20px',
cursor: 'pointer',
}}
>
<motion.h3 layout="position">Card Title</motion.h3>
<AnimatePresence>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
This content is shown when expanded. Click to collapse.
</motion.p>
)}
</AnimatePresence>
</motion.div>
);
}Shared Layout Animations (layoutId)
// Shared layout between components
function ImageGallery() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const images = [
{ id: '1', src: '/img1.jpg', title: 'Image 1' },
{ id: '2', src: '/img2.jpg', title: 'Image 2' },
{ id: '3', src: '/img3.jpg', title: 'Image 3' },
];
return (
<div>
<div className="gallery">
{images.map((image) => (
<motion.div
key={image.id}
layoutId={image.id}
onClick={() => setSelectedId(image.id)}
style={{ cursor: 'pointer' }}
>
<motion.img src={image.src} layoutId={`img-${image.id}`} />
<motion.h5 layoutId={`title-${image.id}`}>{image.title}</motion.h5>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && (
<motion.div
className="overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
>
<motion.div layoutId={selectedId} className="expanded-card">
<motion.img
src={images.find(i => i.id === selectedId)?.src}
layoutId={`img-${selectedId}`}
/>
<motion.h5 layoutId={`title-${selectedId}`}>
{images.find(i => i.id === selectedId)?.title}
</motion.h5>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}Variants: Named Animation States
// Define reusable animation states
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Stagger child animations
delayChildren: 0.2,
},
},
exit: {
opacity: 0,
transition: {
staggerChildren: 0.05,
staggerDirection: -1, // Reverse stagger on exit
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
};
function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{items.map((item, index) => (
<motion.li key={index} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}
// Directional variants
const slideVariants = {
enter: (direction: number) => ({
x: direction > 0 ? 1000 : -1000,
opacity: 0,
}),
center: {
zIndex: 1,
x: 0,
opacity: 1,
},
exit: (direction: number) => ({
zIndex: 0,
x: direction < 0 ? 1000 : -1000,
opacity: 0,
}),
};
function Carousel({ items }: { items: string[] }) {
const [[page, direction], setPage] = useState([0, 0]);
const paginate = (newDirection: number) => {
setPage([page + newDirection, newDirection]);
};
return (
<AnimatePresence initial={false} custom={direction}>
<motion.div
key={page}
custom={direction}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: 'spring', stiffness: 300, damping: 30 },
opacity: { duration: 0.2 },
}}
>
{items[page]}
</motion.div>
</AnimatePresence>
);
}Scroll-Triggered Animations
import { motion, useScroll, useTransform, useInView } from 'framer-motion';
// Basic scroll-triggered animation
function ScrollReveal({ children }: { children: React.ReactNode }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}
// Parallax scroll effect
function ParallaxSection({ children }: { children: React.ReactNode }) {
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%']);
return (
<motion.div style={{ y }}>
{children}
</motion.div>
);
}
// Progress-linked scroll
function ScrollProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1]);
return (
<motion.div
style={{
scaleX,
transformOrigin: 'left',
background: '#005fcc',
height: '4px',
position: 'fixed',
top: 0,
left: 0,
right: 0,
}}
/>
);
}Advanced Patterns
Orchestrated Animations
// Complex orchestration with useAnimation
import { useAnimation, motion } from 'framer-motion';
function SuccessAnimation() {
const controls = useAnimation();
const runAnimation = async () => {
// Step 1: Fade in
await controls.start({ opacity: 1, transition: { duration: 0.3 } });
// Step 2: Scale up
await controls.start({ scale: 1.2, transition: { duration: 0.2 } });
// Step 3: Bounce back
await controls.start({ scale: 1, transition: { type: 'spring', stiffness: 500 } });
// Step 4: Show checkmark
await controls.start({ pathLength: 1, transition: { duration: 0.5 } });
};
return (
<motion.div animate={controls} initial={{ opacity: 0, scale: 0.8 }}>
<motion.svg viewBox="0 0 50 50">
<motion.path
d="M14 27l7.8 7.8L36 16"
fill="none"
stroke="#22c55e"
strokeWidth="3"
initial={{ pathLength: 0 }}
animate={controls}
/>
</motion.svg>
</motion.div>
);
}Performance Tips
- Use
layoutsparingly — Layout animations trigger reflows; use only when needed - Prefer
transformandopacity— These are compositor-only properties - Use
will-changehint — Tell the browser which properties will animate - Animate in batches — Use
staggerChildreninstead of individual delays - Use
mode="wait"for AnimatePresence — Prevents layout jumps during transitions
Best Practices for Production
- Use variants for reusable animations — Define states once, apply across components
- Respect user preferences — Check
prefers-reduced-motionand disable animations - Keep animations subtle — Animations should enhance, not distract
- Test on low-end devices — Ensure animations perform well on slower hardware
- Use layoutId for shared element transitions — Automatic position/size animation
- Leverage
useInViewfor scroll animations — Only animate when visible
Cross-Browser Testing Strategy
Modern CSS features often have varying levels of browser support, making a systematic cross-browser testing strategy essential. Before using any CSS feature in production, verify its support status and implement appropriate fallbacks for browsers that haven't yet implemented it.
Progressive Enhancement with @supports
Use the @supports at-rule to provide fallback styles for browsers that don't support specific CSS features:
/* Base styles for all browsers */
.grid-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.grid-container > * {
flex: 1 1 300px;
}
/* Enhanced layout for browsers supporting CSS Grid */
@supports (display: grid) {
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.grid-container > * {
flex: none;
}
}
/* Further enhancement with subgrid support */
@supports (grid-template-columns: subgrid) {
.grid-container {
grid-template-columns: subgrid;
}
}Visual Regression Testing
Implement visual regression testing to catch unintended layout shifts and styling changes. Tools like Percy, Chromatic, or Playwright's screenshot comparison can detect visual differences across browsers and screen sizes:
const { test, expect } = require('@playwright/test');
test('responsive layout matches design', async ({ page }) => {
await page.goto('/components/dashboard');
// Test at multiple viewport sizes
for (const viewport of [
{ width: 375, height: 812, name: 'mobile' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 1440, height: 900, name: 'desktop' },
]) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await expect(page).toHaveScreenshot(
`dashboard-${viewport.name}.png`,
{ maxDiffPixels: 100 }
);
}
});Browser Compatibility Testing Matrix
Maintain a testing matrix that covers the browsers and versions your users actually use. Use analytics data to determine your browser support baseline, then configure tools like Browserslist to automatically handle polyfilling and prefixing:
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead",
"not ie 11"
]
}This data-driven approach ensures you're spending testing effort where it matters most, rather than trying to support every possible browser configuration.
Community Resources and Further Learning
The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.
Curated Learning Pathways
Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.
Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.
Contributing to Open Source
Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.
# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
# Run the project's contribution setup
npm run setup:dev
npm run test # Ensure tests pass before making changes
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
Closes #1234"
git push origin fix/issue-descriptionBuilding a Technical Knowledge Base
Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.
Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.
Staying Current with Industry Trends
Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.
Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.
Mentorship and Knowledge Sharing
Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.
Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.
Conclusion
Framer Motion (Motion) provides a comprehensive animation system for React that handles everything from simple hover effects to complex page transitions. Its declarative API, physics-based animations, and automatic layout animations make it possible to create polished, performant animations with minimal code. Whether you're building a landing page with scroll-triggered animations, a dashboard with interactive charts, or a mobile app with gesture-based navigation, Motion has the tools you need.