
Introduction
Animations play a crucial role in modern web applications, providing visual feedback, improving user experience, and making interfaces feel more responsive and engaging. React offers several approaches to implement animations, from simple CSS transitions to complex animation libraries. This chapter explores various React animations techniques and tools available in the React ecosystem.
Why Animations Matter in React Applications
Animations serve multiple purposes in web applications:
- User Experience Enhancement: Smooth transitions guide users through interface changes
- Visual Feedback: Animations provide immediate feedback for user interactions
- Attention Direction: Animations can guide user attention to important elements
- State Communication: Visual cues help users understand application state changes
- Professional Polish: Well-implemented animations make applications feel more refined
CSS-Based Animations in React
CSS Transitions
The simplest way to add animations to React components is through CSS transitions:
import React, { useState } from 'react';
import './Button.css';
const AnimatedButton = () => {
 const [isHovered, setIsHovered] = useState(false);
 return (
  <button
   className={`animated-button ${isHovered ? 'hovered' : ''}`}
   onMouseEnter={() => setIsHovered(true)}
   onMouseLeave={() => setIsHovered(false)}
  >
   Hover Me
  </button>
 );
};
export default AnimatedButton;
/* Button.css */
.animated-button {
 padding: 12px 24px;
 background-color: #007bff;
 color: white;
 border: none;
 border-radius: 4px;
 cursor: pointer;
 transition: all 0.3s ease;
 transform: scale(1);
}
.animated-button.hovered {
 background-color: #0056b3;
 transform: scale(1.05);
 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
CSS Keyframe Animations
For more complex animations, CSS keyframes provide greater control:
import React, { useState } from 'react';
import './LoadingSpinner.css';
const LoadingSpinner = ({ isLoading }) => {
 if (!isLoading) return null;
 return (
  <div className="spinner-container">
   <div className="spinner"></div>
   <p>Loading...</p>
  </div>
 );
};
export default LoadingSpinner;
/* LoadingSpinner.css */
.spinner-container {
 display: flex;
 flex-direction: column;
 align-items: center;
 padding: 20px;
}
.spinner {
 width: 40px;
 height: 40px;
 border: 4px solid #f3f3f3;
 border-top: 4px solid #007bff;
 border-radius: 50%;
 animation: spin 1s linear infinite;
}
@keyframes spin {
 0% { transform: rotate(0deg); }
 100% { transform: rotate(360deg); }
}
@keyframes fadeIn {
 from { opacity: 0; }
 to { opacity: 1; }
}
.spinner-container {
 animation: fadeIn 0.3s ease-in;
}
React Transition Group
React Transition Group is the official library for managing component transitions in React applications.
Installation
npm install react-transition-group
npm install --save-dev @types/react-transition-group # For TypeScript
CSSTransition Component
The CSSTransition component applies CSS classes during different transition phases:
import React, { useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './Modal.css';
const Modal = () => {
 const [showModal, setShowModal] = useState(false);
 return (
  <div>
   <button onClick={() => setShowModal(true)}>
    Open Modal
   </button>
  Â
   <CSSTransition
    in={showModal}
    timeout={300}
    classNames="modal"
    unmountOnExit
   >
    <div className="modal-backdrop" onClick={() => setShowModal(false)}>
     <div className="modal-content" onClick={(e) => e.stopPropagation()}>
      <h2>Modal Title</h2>
      <p>This is modal content</p>
      <button onClick={() => setShowModal(false)}>Close</button>
     </div>
    </div>
   </CSSTransition>
  </div>
 );
};
export default Modal;
/* Modal.css */
.modal-backdrop {
 position: fixed;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 background-color: rgba(0, 0, 0, 0.5);
 display: flex;
 justify-content: center;
 align-items: center;
 z-index: 1000;
}
.modal-content {
 background: white;
 padding: 20px;
 border-radius: 8px;
 max-width: 500px;
 width: 90%;
}
/* Enter transition */
.modal-enter {
 opacity: 0;
 transform: scale(0.9);
}
.modal-enter-active {
 opacity: 1;
 transform: scale(1);
 transition: opacity 300ms, transform 300ms;
}
/* Exit transition */
.modal-exit {
 opacity: 1;
 transform: scale(1);
}
.modal-exit-active {
 opacity: 0;
 transform: scale(0.9);
 transition: opacity 300ms, transform 300ms;
}
TransitionGroup for Lists
TransitionGroup manages transitions for lists of components:
import React, { useState } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import './TodoList.css';
const TodoList = () => {
 const [todos, setTodos] = useState([
  { id: 1, text: 'Learn React' },
  { id: 2, text: 'Build an app' }
 ]);
 const [inputValue, setInputValue] = useState('');
 const addTodo = () => {
  if (inputValue.trim()) {
   setTodos([...todos, {
    id: Date.now(),
    text: inputValue
   }]);
   setInputValue('');
  }
 };
 const removeTodo = (id) => {
  setTodos(todos.filter(todo => todo.id !== id));
 };
 return (
  <div className="todo-container">
   <div className="add-todo">
    <input
     value={inputValue}
     onChange={(e) => setInputValue(e.target.value)}
     placeholder="Add new todo..."
     onKeyPress={(e) => e.key === 'Enter' && addTodo()}
    />
    <button onClick={addTodo}>Add</button>
   </div>
  Â
   <TransitionGroup className="todo-list">
    {todos.map(todo => (
     <CSSTransition
      key={todo.id}
      timeout={300}
      classNames="todo-item"
     >
      <div className="todo-item">
       <span>{todo.text}</span>
       <button onClick={() => removeTodo(todo.id)}>×</button>
      </div>
     </CSSTransition>
    ))}
   </TransitionGroup>
  </div>
 );
};
export default TodoList;
/* TodoList.css */
.todo-container {
 max-width: 400px;
 margin: 0 auto;
 padding: 20px;
}
.add-todo {
 display: flex;
 margin-bottom: 20px;
}
.add-todo input {
 flex: 1;
 padding: 8px;
 border: 1px solid #ddd;
 border-radius: 4px;
 margin-right: 8px;
}
.todo-list {
 list-style: none;
 padding: 0;
}
.todo-item {
 display: flex;
 justify-content: space-between;
 align-items: center;
 padding: 12px;
 margin-bottom: 8px;
 background: #f8f9fa;
 border-radius: 4px;
 border-left: 4px solid #007bff;
}
/* List item animations */
.todo-item-enter {
 opacity: 0;
 transform: translateX(-100%);
}
.todo-item-enter-active {
 opacity: 1;
 transform: translateX(0);
 transition: opacity 300ms, transform 300ms;
}
.todo-item-exit {
 opacity: 1;
 transform: translateX(0);
}
.todo-item-exit-active {
 opacity: 0;
 transform: translateX(100%);
 transition: opacity 300ms, transform 300ms;
}
Framer Motion
Framer Motion is a popular animation library that provides a more declarative approach to animations in React.
Installation
npm install framer-motion
Basic Animations
import React from 'react';
import { motion } from 'framer-motion';
const BasicAnimation = () => {
 return (
  <div style={{ padding: '50px' }}>
   <motion.div
    initial={{ opacity: 0, y: -50 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.5 }}
    style={{
     width: 100,
     height: 100,
     backgroundColor: '#007bff',
     borderRadius: '8px'
    }}
   />
  </div>
 );
};
export default BasicAnimation;
Hover and Tap Animations
import React from 'react';
import { motion } from 'framer-motion';
const InteractiveCard = () => {
 return (
  <motion.div
   className="card"
   whileHover={{
    scale: 1.05,
    boxShadow: "0 10px 25px rgba(0,0,0,0.2)"
   }}
   whileTap={{ scale: 0.95 }}
   transition={{ type: "spring", stiffness: 300 }}
   style={{
    width: 200,
    height: 150,
    backgroundColor: 'white',
    borderRadius: '12px',
    padding: '20px',
    cursor: 'pointer',
    boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
   }}
  >
   <h3>Interactive Card</h3>
   <p>Hover and click me!</p>
  </motion.div>
 );
};
export default InteractiveCard;
Layout Animations
Framer Motion’s layoutId provides smooth transitions between components:
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const LayoutAnimation = () => {
 const [selectedId, setSelectedId] = useState(null);
Â
 const items = [
  { id: 1, title: 'Item 1', content: 'Content for item 1' },
  { id: 2, title: 'Item 2', content: 'Content for item 2' },
  { id: 3, title: 'Item 3', content: 'Content for item 3' }
 ];
 return (
  <div style={{ padding: '20px' }}>
   <div style={{ display: 'grid', gap: '16px' }}>
    {items.map(item => (
     <motion.div
      key={item.id}
      layoutId={item.id}
      onClick={() => setSelectedId(item.id)}
      style={{
       padding: '20px',
       backgroundColor: '#f8f9fa',
       borderRadius: '8px',
       cursor: 'pointer'
      }}
      whileHover={{ backgroundColor: '#e9ecef' }}
     >
      <motion.h3>{item.title}</motion.h3>
     </motion.div>
    ))}
   </div>
   <AnimatePresence>
    {selectedId && (
     <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      onClick={() => setSelectedId(null)}
      style={{
       position: 'fixed',
       top: 0,
       left: 0,
       width: '100%',
       height: '100%',
       backgroundColor: 'rgba(0,0,0,0.5)',
       display: 'flex',
       justifyContent: 'center',
       alignItems: 'center'
      }}
     >
      <motion.div
       layoutId={selectedId}
       style={{
        padding: '40px',
        backgroundColor: 'white',
        borderRadius: '12px',
        maxWidth: '500px',
        width: '90%'
       }}
       onClick={(e) => e.stopPropagation()}
      >
       <motion.h3>
        {items.find(item => item.id === selectedId)?.title}
       </motion.h3>
       <motion.p>
        {items.find(item => item.id === selectedId)?.content}
       </motion.p>
       <button onClick={() => setSelectedId(null)}>Close</button>
      </motion.div>
     </motion.div>
    )}
   </AnimatePresence>
  </div>
 );
};
export default LayoutAnimation;
Variants for Complex Animations
Variants help organize complex animations:
import React, { useState } from 'react';
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
 }
};
const StaggeredList = () => {
 const [isVisible, setIsVisible] = useState(false);
Â
 const items = ['First', 'Second', 'Third', 'Fourth', 'Fifth'];
 return (
  <div style={{ padding: '20px' }}>
   <button onClick={() => setIsVisible(!isVisible)}>
    Toggle List
   </button>
  Â
   {isVisible && (
    <motion.ul
     variants={containerVariants}
     initial="hidden"
     animate="visible"
     style={{ listStyle: 'none', padding: 0 }}
    >
     {items.map((item, index) => (
      <motion.li
       key={item}
       variants={itemVariants}
       style={{
        padding: '12px',
        margin: '8px 0',
        backgroundColor: '#007bff',
        color: 'white',
        borderRadius: '4px'
       }}
      >
       {item} Item
      </motion.li>
     ))}
    </motion.ul>
   )}
  </div>
 );
};
export default StaggeredList;
React Spring
React Spring is another powerful animation library focusing on spring-physics based animations.
Installation
npm install @react-spring/web
Basic Spring Animation
import React, { useState } from 'react';
import { useSpring, animated } from '@react-spring/web';
const SpringAnimation = () => {
 const [flip, setFlip] = useState(false);
Â
 const springs = useSpring({
  to: {
   opacity: flip ? 1 : 0,
   transform: `perspective(600px) rotateX(${flip ? 180 : 0}deg)`,
   backgroundColor: flip ? '#007bff' : '#28a745'
  },
  config: { mass: 5, tension: 500, friction: 80 }
 });
 return (
  <div style={{ padding: '50px' }}>
   <animated.div
    style={{
     width: 200,
     height: 200,
     borderRadius: '8px',
     cursor: 'pointer',
     display: 'flex',
     alignItems: 'center',
     justifyContent: 'center',
     color: 'white',
     fontSize: '24px',
     ...springs
    }}
    onClick={() => setFlip(!flip)}
   >
    Click me!
   </animated.div>
  </div>
 );
};
export default SpringAnimation;
Trail Animation
import React from 'react';
import { useTrail, animated } from '@react-spring/web';
const TrailAnimation = () => {
 const items = ['R', 'e', 'a', 'c', 't'];
Â
 const trail = useTrail(items.length, {
  from: { opacity: 0, transform: 'translate3d(0,-40px,0)' },
  to: { opacity: 1, transform: 'translate3d(0,0px,0)' },
  config: { mass: 5, tension: 2000, friction: 200 }
 });
 return (
  <div style={{ padding: '50px' }}>
   {trail.map((style, index) => (
    <animated.div
     key={items[index]}
     style={{
      ...style,
      display: 'inline-block',
      fontSize: '48px',
      fontWeight: 'bold',
      color: '#007bff',
      margin: '0 4px'
     }}
    >
     {items[index]}
    </animated.div>
   ))}
  </div>
 );
};
export default TrailAnimation;
Performance Considerations
CSS vs JavaScript Animations
CSS Animations:
- Better performance for simple animations
- Hardware acceleration support
- Less JavaScript overhead
- Limited control and interactivity
JavaScript Animations:
- Better control and complex logic
- Dynamic animation parameters
- Easier to debug and test
- Can be more resource-intensive
Animation Best Practices
- Use CSS transforms instead of changing layout properties:
// Good - uses transform
const goodAnimation = {
 transform: 'translateX(100px)'
};
// Bad - causes layout recalculation
const badAnimation = {
 left: '100px'
};
- Prefer opacity and transform properties:
/* Performant properties */
.animated-element {
 transition: opacity 0.3s, transform 0.3s;
}
/* Avoid animating these */
.slow-animation {
 transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}
- Use will-change for heavy animations:
.heavy-animation {
 will-change: transform;
}
Memory Management
Clean up animations to prevent memory leaks:
import React, { useEffect, useRef } from 'react';
const AnimationCleanup = () => {
 const elementRef = useRef(null);
 const animationRef = useRef(null);
 useEffect(() => {
  const element = elementRef.current;
 Â
  const animate = () => {
   // Animation logic here
   animationRef.current = requestAnimationFrame(animate);
  };
 Â
  animate();
  return () => {
   if (animationRef.current) {
    cancelAnimationFrame(animationRef.current);
   }
  };
 }, []);
 return <div ref={elementRef}>Animated content</div>;
};
export default AnimationCleanup;
Common Animation Patterns
Loading Animations
import React from 'react';
import { motion } from 'framer-motion';
const LoadingDots = () => {
 const bounceTransition = {
  y: {
   duration: 0.4,
   repeat: Infinity,
   repeatType: "reverse",
   ease: "easeOut"
  }
 };
 return (
  <div style={{ display: 'flex', justifyContent: 'center', gap: '4px' }}>
   {[0, 1, 2].map(i => (
    <motion.div
     key={i}
     animate={{ y: [0, -20, 0] }}
     transition={{
      ...bounceTransition,
      delay: i * 0.1
     }}
     style={{
      width: 8,
      height: 8,
      backgroundColor: '#007bff',
      borderRadius: '50%'
     }}
    />
   ))}
  </div>
 );
};
export default LoadingDots;
Page Transitions
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const pageVariants = {
 initial: {
  opacity: 0,
  x: "-100vw",
  scale: 0.8
 },
 in: {
  opacity: 1,
  x: 0,
  scale: 1
 },
 out: {
  opacity: 0,
  x: "100vw",
  scale: 1.2
 }
};
const pageTransition = {
 type: "tween",
 ease: "anticipate",
 duration: 0.5
};
const PageTransition = ({ children, currentPage }) => {
 return (
  <AnimatePresence mode="wait">
   <motion.div
    key={currentPage}
    initial="initial"
    animate="in"
    exit="out"
    variants={pageVariants}
    transition={pageTransition}
   >
    {children}
   </motion.div>
  </AnimatePresence>
 );
};
export default PageTransition;
Testing Animated Components
Testing with React Testing Library
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import AnimatedButton from './AnimatedButton';
describe('AnimatedButton', () => {
 test('applies hover styles on mouse enter', async () => {
  render(<AnimatedButton />);
  const button = screen.getByRole('button');
 Â
  fireEvent.mouseEnter(button);
 Â
  await waitFor(() => {
   expect(button).toHaveClass('hovered');
  });
 });
 test('removes hover styles on mouse leave', async () => {
  render(<AnimatedButton />);
  const button = screen.getByRole('button');
 Â
  fireEvent.mouseEnter(button);
  fireEvent.mouseLeave(button);
 Â
  await waitFor(() => {
   expect(button).not.toHaveClass('hovered');
  });
 });
});
Mocking Animation Libraries
// __mocks__/framer-motion.js
const actual = jest.requireActual('framer-motion');
module.exports = {
 ...actual,
 motion: {
  div: ({ children, ...props }) => <div {...props}>{children}</div>,
  button: ({ children, ...props }) => <button {...props}>{children}</button>,
  // Add other elements as needed
 },
 AnimatePresence: ({ children }) => children,
};
Accessibility in Animations
Respecting User Preferences
import React, { useEffect, useState } from 'react';
const useReducedMotion = () => {
 const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
 useEffect(() => {
  const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
  setPrefersReducedMotion(mediaQuery.matches);
  const handler = (event) => setPrefersReducedMotion(event.matches);
  mediaQuery.addEventListener('change', handler);
  return () => mediaQuery.removeEventListener('change', handler);
 }, []);
 return prefersReducedMotion;
};
const AccessibleAnimation = () => {
 const prefersReducedMotion = useReducedMotion();
 return (
  <div
   className={`animated-element ${prefersReducedMotion ? 'reduced-motion' : ''}`}
   style={{
    transition: prefersReducedMotion ? 'none' : 'all 0.3s ease',
    transform: 'translateX(0)'
   }}
  >
   Content with accessible animation
  </div>
 );
};
export default AccessibleAnimation;
CSS Media Queries for Reduced Motion
@media (prefers-reduced-motion: reduce) {
 *,
 *::before,
 *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  scroll-behavior: auto !important;
 }
}
Conclusion
React provides multiple approaches to implement animations, from simple CSS transitions to complex physics-based animations with libraries like Framer Motion and React Spring. Choose the right approach based on your needs:
- CSS Animations: For simple, performant animations
- React Transition Group: For managing component lifecycle transitions
- Framer Motion: For declarative, feature-rich animations
- React Spring: For physics-based, smooth animations
Remember to consider performance, accessibility, and user preferences when implementing animations. Well-crafted animations enhance user experience, while poorly implemented ones can degrade performance and usability.
The key to successful animations in React is understanding when and how to use each technique appropriately, always keeping the user experience as the primary focus.