React.js Fundamentals
Animations in React
Animations in React
Animations bring your React applications to life by providing visual feedback and improving user experience. Learn how to implement animations using CSS transitions and the powerful Framer Motion library.
CSS Transitions and Animations
Start with basic CSS transitions for simple animations:
import { useState } from 'react';
import './Button.css';
function AnimatedButton() {
const [isHovered, setIsHovered] = useState(false);
return (
<button
className={`animated-button ${isHovered ? 'hovered' : ''}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
Hover Me
</button>
);
}
CSS file (Button.css):
.animated-button {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
transform: scale(1);
}
.animated-button.hovered {
background: #0056b3;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
Note: CSS transitions are great for simple animations. For more complex animations with multiple states and sequences, consider using a library like Framer Motion.
Installing and Setting Up Framer Motion
Framer Motion is a production-ready motion library for React. Install it with:
npm install framer-motion
Basic usage example:
import { motion } from 'framer-motion';
function FadeInBox() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
style={{
width: 200,
height: 200,
background: '#007bff',
borderRadius: 8,
}}
>
I fade in!
</motion.div>
);
}
Tip: Framer Motion works with any HTML or SVG element. Just replace the element tag with motion.element (e.g., motion.div, motion.button, motion.svg).
Hover, Tap, and Focus Animations
Add interactive animations with gesture props:
import { motion } from 'framer-motion';
function InteractiveCard() {
return (
<motion.div
className="card"
whileHover={{
scale: 1.05,
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.2)',
}}
whileTap={{ scale: 0.95 }}
whileFocus={{ outline: '2px solid #007bff' }}
transition={{ type: 'spring', stiffness: 300 }}
style={{
width: 300,
padding: 20,
background: 'white',
borderRadius: 8,
cursor: 'pointer',
}}
>
<h3>Interactive Card</h3>
<p>Hover, tap, or focus on me!</p>
</motion.div>
);
}
// Button with multiple gesture animations
function AnimatedButton() {
return (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
style={{
padding: '12px 24px',
background: '#28a745',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontSize: 16,
}}
>
Click Me
</motion.button>
);
}
AnimatePresence for Exit Animations
Animate components when they're removed from the DOM:
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
function NotificationList() {
const [notifications, setNotifications] = useState([
{ id: 1, message: 'Success! Your changes have been saved.' },
{ id: 2, message: 'New message received.' },
]);
const removeNotification = (id) => {
setNotifications(notifications.filter((n) => n.id !== id));
};
return (
<div style={{ padding: 20 }}>
<AnimatePresence>
{notifications.map((notification) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
transition={{ duration: 0.3 }}
style={{
padding: 16,
marginBottom: 12,
background: '#e7f5ff',
borderRadius: 8,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<p>{notification.message}</p>
<button onClick={() => removeNotification(notification.id)}>
×
</button>
</motion.div>
))}
</AnimatePresence>
</div>
);
}
Warning: Always use unique keys when animating lists with AnimatePresence. Without proper keys, exit animations may not work correctly.
Variants for Complex Animations
Use variants to create reusable animation configurations:
import { motion } from 'framer-motion';
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
},
};
function StaggeredList() {
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
style={{ listStyle: 'none', padding: 0 }}
>
{items.map((item, index) => (
<motion.li
key={index}
variants={itemVariants}
style={{
padding: 20,
marginBottom: 12,
background: '#f8f9fa',
borderRadius: 8,
}}
>
{item}
</motion.li>
))}
</motion.ul>
);
}
Page Transitions
Create smooth transitions between routes:
import { motion, AnimatePresence } from 'framer-motion';
import { Routes, Route, useLocation } from 'react-router-dom';
const pageVariants = {
initial: {
opacity: 0,
x: '-100vw',
},
in: {
opacity: 1,
x: 0,
},
out: {
opacity: 0,
x: '100vw',
},
};
const pageTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.5,
};
function Home() {
return (
<motion.div
initial="initial"
animate="in"
exit="out"
variants={pageVariants}
transition={pageTransition}
>
<h1>Home Page</h1>
<p>Welcome to the home page!</p>
</motion.div>
);
}
function About() {
return (
<motion.div
initial="initial"
animate="in"
exit="out"
variants={pageVariants}
transition={pageTransition}
>
<h1>About Page</h1>
<p>Learn more about us!</p>
</motion.div>
);
}
function AnimatedRoutes() {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</AnimatePresence>
);
}
Tip: Use mode="wait" in AnimatePresence to ensure the exit animation completes before the next component enters.
Drag and Drop Animations
Make elements draggable with built-in physics:
import { motion } from 'framer-motion';
function DraggableBox() {
return (
<motion.div
drag
dragConstraints={{
top: -50,
left: -50,
right: 50,
bottom: 50,
}}
dragElastic={0.2}
whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
style={{
width: 150,
height: 150,
background: '#6c757d',
borderRadius: 8,
cursor: 'grab',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
}}
>
Drag me!
</motion.div>
);
}
// Drag with snap back
function SnapBackBox() {
return (
<motion.div
drag
dragSnapToOrigin
whileDrag={{ scale: 1.1 }}
style={{
width: 150,
height: 150,
background: '#17a2b8',
borderRadius: 8,
cursor: 'grab',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
}}
>
I snap back!
</motion.div>
);
}
Layout Animations
Automatically animate layout changes with the layout prop:
import { useState } from 'react';
import { motion } from 'framer-motion';
function LayoutAnimation() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
style={{
background: '#20c997',
borderRadius: 8,
padding: 20,
cursor: 'pointer',
width: isExpanded ? 400 : 200,
height: isExpanded ? 300 : 100,
}}
>
<motion.h3 layout>Expandable Box</motion.h3>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
This content appears when expanded!
</motion.p>
)}
</motion.div>
);
}
// Grid layout animation
function AnimatedGrid() {
const [items, setItems] = useState([1, 2, 3, 4, 5, 6]);
const shuffle = () => {
setItems([...items].sort(() => Math.random() - 0.5));
};
return (
<div>
<button onClick={shuffle}>Shuffle</button>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 16,
marginTop: 20,
}}
>
{items.map((item) => (
<motion.div
key={item}
layout
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{
padding: 40,
background: '#fd7e14',
borderRadius: 8,
color: 'white',
textAlign: 'center',
}}
>
{item}
</motion.div>
))}
</div>
</div>
);
}
Custom Animation Hooks
Create reusable animation hooks:
import { useAnimation } from 'framer-motion';
import { useEffect } from 'react';
function useScrollAnimation() {
const controls = useAnimation();
useEffect(() => {
const handleScroll = () => {
const scrollY = window.scrollY;
if (scrollY > 100) {
controls.start({ opacity: 1, y: 0 });
} else {
controls.start({ opacity: 0, y: 50 });
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [controls]);
return controls;
}
// Using the custom hook
function ScrollReveal() {
const controls = useScrollAnimation();
return (
<motion.div
animate={controls}
initial={{ opacity: 0, y: 50 }}
style={{
padding: 40,
background: '#e83e8c',
borderRadius: 8,
color: 'white',
marginTop: 200,
}}
>
Scroll down to reveal me!
</motion.div>
);
}
Loading and Skeleton Animations
Create smooth loading states:
import { motion } from 'framer-motion';
function SkeletonLoader() {
return (
<div>
{[1, 2, 3].map((item) => (
<motion.div
key={item}
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
}}
style={{
height: 100,
background: '#e9ecef',
borderRadius: 8,
marginBottom: 12,
}}
/>
))}
</div>
);
}
// Spinner animation
function Spinner() {
return (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: 'linear',
}}
style={{
width: 50,
height: 50,
border: '4px solid #e9ecef',
borderTopColor: '#007bff',
borderRadius: '50%',
}}
/>
);
}
Exercise 1: Create a modal component with entrance and exit animations using Framer Motion. The modal should slide in from the bottom when opened and fade out when closed. Add a backdrop with fade animation.
Exercise 2: Build an image gallery with hover effects, click-to-expand animations, and smooth transitions between grid and fullscreen views. Use AnimatePresence for the fullscreen overlay.
Exercise 3: Implement a multi-step form wizard with page transition animations. Each step should slide in from the right when moving forward and from the left when going back. Add progress indicator animations.