DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 28: React Animations

React Animations

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

  1. Use CSS transforms instead of changing layout properties:
// Good - uses transform
const goodAnimation = {
  transform: 'translateX(100px)'
};

// Bad - causes layout recalculation
const badAnimation = {
  left: '100px'
};
  1. 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;
}
  1. 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.

Related Articles

Scroll to Top