#react /
Framer Motion in Practice: Building Smooth React Animations
From Framer Motion basics to advanced animation orchestration, covering layout animations, gesture interactions, page transitions, and other practical scenarios to bring your React application to life.
Goal
Animation is a key手段 for improving user experience, but animation development in React has always been a pain point. CSS animations are difficult to maintain, and third-party library APIs are inconsistent. Framer Motion (now renamed Motion) makes React animation development simple yet powerful through its declarative API and powerful animation engine. This article systematically explains Framer Motion's core usage from basics to practice.
Background
React Animation Pain Points
Before Framer Motion, there were several ways to do animations in React:
- CSS transition/animation: Sufficient for simple scenarios, but complex orchestration is difficult
- React Transition Group: Only provides enter/exit states, requiring manual animation management
- GSAP: Powerful but large in size, and not fully aligned with React's declarative philosophy
- react-spring: Spring physics model has a steep learning curve
Framer Motion addresses these issues by providing:
- Declarative API: Consistent with React's philosophy
- Layout animations: Smooth layout transitions without manual calculation
- Gesture system: Support for drag, swipe, zoom, and other gestures
- Page transitions: Deep integration with Next.js
Basic Animations
Simple Animations
import { motion } from 'framer-motion';
// Basic fade-in
function FadeIn() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h1>Fade In Effect</h1>
</motion.div>
);
}
// Hover animation
function HoverCard() {
return (
<motion.div
whileHover={{ scale: 1.05, y: -5 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="card"
>
<p>Hover over me</p>
</motion.div>
);
}
Animation Properties Explained
<motion.div
// Initial state
initial={{ opacity: 0, x: -100 }}
// Target state
animate={{ opacity: 1, x: 0 }}
// Exit state (with AnimatePresence)
exit={{ opacity: 0, x: 100 }}
// Transition configuration
transition={{
duration: 0.6, // Duration
ease: [0.25, 0.46, 0.45, 0.94], // Bezier curve
delay: 0.2, // Delay
type: 'spring', // Spring animation
stiffness: 260, // Spring stiffness
damping: 20, // Spring damping
mass: 1, // Spring mass
}}
>
Content
</motion.div>
Animation Variants
// Define variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Children animate sequentially
delayChildren: 0.3,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 300,
damping: 24,
},
},
};
// Use variants
function StaggerList() {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item, index) => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
);
}
Layout Animations
Layout Property
One of Framer Motion's most powerful features is layout animations, enabling smooth layout transitions without manual calculation:
import { motion, LayoutGroup } from 'framer-motion';
function LayoutDemo() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<LayoutGroup>
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
style={{
width: isExpanded ? 400 : 200,
height: isExpanded ? 300 : 100,
backgroundColor: '#3b82f6',
}}
transition={{
type: 'spring',
stiffness: 500,
damping: 30,
}}
>
<p>Click to toggle size</p>
</motion.div>
</LayoutGroup>
);
}
Swap Animation
function SwapAnimation() {
const [items, setItems] = useState(['A', 'B', 'C']);
const swap = () => {
setItems(prev => {
const newItems = [...prev];
[newItems[0], newItems[2]] = [newItems[2], newItems[0]];
return newItems;
});
};
return (
<div>
<button onClick={swap}>Swap</button>
<motion.div layout className="container">
{items.map(item => (
<motion.div
key={item}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{item}
</motion.div>
))}
</motion.div>
</div>
);
}
Gesture Interactions
Drag
function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{
top: -100,
bottom: 100,
left: -200,
right: 200,
}}
dragElastic={0.1}
whileDrag={{ scale: 1.1, boxShadow: '0 20px 40px rgba(0,0,0,0.3)' }}
onDrag={(event, info) => {
console.log('Dragging:', info.point);
}}
onDragEnd={(event, info) => {
console.log('Drag ended:', info.offset);
}}
>
Drag me
</motion.div>
);
}
Drag Sorting
function DragSort() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
return (
<Reorder.Group axis="y" values={items} onReorder={setItems}>
{items.map(item => (
<Reorder.Item key={item} value={item}>
<div className="item">{item}</div>
</Reorder.Item>
))}
</Reorder.Group>
);
}
Gesture Detection
function GestureDemo() {
return (
<motion.div
// Hover
whileHover={{ scale: 1.1 }}
// Tap
whileTap={{ scale: 0.95 }}
// Focus
whileFocus={{ outline: '2px solid blue' }}
// Drag
drag
// Gesture callbacks
onHoverStart={() => console.log('hover start')}
onHoverEnd={() => console.log('hover end')}
onTap={() => console.log('tap')}
onTapStart={() => console.log('tap start')}
onTapCancel={() => console.log('tap cancel')}
>
Gesture Interaction
</motion.div>
);
}
Page Transitions
AnimatePresence
import { AnimatePresence } from 'framer-motion';
function PageTransition() {
const [currentPage, setCurrentPage] = useState('home');
return (
<div>
<nav>
<button onClick={() => setCurrentPage('home')}>Home</button>
<button onClick={() => setCurrentPage('about')}>About</button>
</nav>
<AnimatePresence mode="wait">
{currentPage === 'home' && (
<motion.div
key="home"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
<h1>Home</h1>
</motion.div>
)}
{currentPage === 'about' && (
<motion.div
key="about"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
<h1>About</h1>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Integration with Next.js App Router
// app/layout.tsx
'use client';
import { motion } from 'framer-motion';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
</body>
</html>
);
}
Advanced Animation Orchestration
Timeline Animation
function TimelineAnimation() {
return (
<motion.div
animate={{
x: [0, 100, 100, 0],
y: [0, 0, 100, 100],
rotate: [0, 90, 180, 270],
}}
transition={{
duration: 2,
times: [0, 0.3, 0.6, 1],
ease: 'easeInOut',
repeat: Infinity,
}}
/>
);
}
Scroll-Driven Animation
import { useScroll, useTransform } from 'framer-motion';
function ScrollAnimation() {
const { scrollYProgress } = useScroll();
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 0.5], [1, 0.8]);
return (
<motion.div style={{ opacity, scale }}>
Fade out and scale down on scroll
</motion.div>
);
}
// Animation based on element scroll position
function ElementScrollAnimation() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
});
const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]);
return (
<div ref={ref}>
<motion.div style={{ rotate }}>
Rotate on scroll
</motion.div>
</div>
);
}
Performance Optimization
Using layoutId for Animation Optimization
// ❌ Not recommended: Each card animates independently
{items.map(item => (
<motion.div
key={item.id}
animate={{ x: item.x, y: item.y }}
/>
))}
// ✅ Recommended: Use layoutId for animation orchestration
{items.map(item => (
<motion.div
key={item.id}
layoutId={`card-${item.id}`}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
))}
Avoiding Unnecessary Re-renders
// ❌ Not recommended: Creates new animation objects on every render
function Bad() {
return <motion.div animate={{ x: Math.random() * 100 }} />;
}
// ✅ Recommended: Use useMemo or variants
function Good() {
const variants = useMemo(() => ({
idle: { x: 0 },
animate: { x: 100 },
}), []);
return <motion.div variants={variants} animate="animate" />;
}
Conclusion
Framer Motion makes React animation development simple and efficient through its declarative API and powerful animation engine. Key takeaways:
- Declarative animation: initial -> animate -> exit three-phase model
- Layout animations: layout property enables smooth transitions without calculation
- Gesture system: Native support for drag, hover, click, and other gestures
- Animation orchestration: variants, stagger, timeline for complex animation sequences
- Performance optimization: Use layoutId appropriately, avoid unnecessary re-renders
Animation is not just flashy decoration -- it is an important means of improving user experience. Mastering Framer Motion will make your React applications more vivid and fluid.