Skip to main content

Overview

Animations bring your React applications to life, providing visual feedback and improving user experience. This section covers both CSS-based animations and the popular Framer Motion library.

CSS Animations

Built-in browser animations with transitions and keyframes

Framer Motion

Powerful animation library for React with declarative API

Exit Animations

Animate components when they’re removed from the DOM

Layout Animations

Smooth layout changes and element repositioning

CSS Transitions

CSS transitions are the simplest way to add animations to your React components.

Basic Transition

.challenge-item-details-icon {
  display: inline-block;
  font-size: 0.85rem;
  margin-left: 0.25rem;
  transition: transform 0.3s ease-out;
}

.challenge-item-details.expanded .challenge-item-details-icon {
  transform: rotate(180deg);
}
The transition property defines which CSS properties should animate, the duration, and the timing function (ease-out, ease-in, linear, etc.).

CSS Keyframe Animations

For more complex animations, use CSS @keyframes.
.modal {
  top: 10%;
  border-radius: 6px;
  padding: 1.5rem;
  width: 30rem;
  max-width: 90%;
  z-index: 10;
  animation: slide-up-fade-in 0.3s ease-out forwards;
}

@keyframes slide-up-fade-in {
  0% {
    transform: translateY(30px);
    opacity: 0;
  }

  100% {
    transform: translateY(0);
    opacity: 1;
  }
}
Use forwards in the animation property to maintain the final state after the animation completes.

Common Keyframe Patterns

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.element {
  animation: fade-in 0.3s ease-in;
}

Framer Motion

Framer Motion is a production-ready animation library for React with a simple, declarative API.

Installation

npm install framer-motion

Basic Animation

import { createPortal } from 'react-dom';
import { motion } from 'framer-motion';

export default function Modal({ title, children, onClose }) {
  return createPortal(
    <>
      <div className="backdrop" onClick={onClose} />
      <motion.dialog
        initial={{ opacity: 0, y: 30 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: 30 }}
        open
        className="modal"
      >
        <h2>{title}</h2>
        {children}
      </motion.dialog>
    </>,
    document.getElementById('modal')
  );
}
Framer Motion uses motion components (like motion.div, motion.dialog) which are enhanced versions of HTML elements with animation capabilities.

Animation Props

The starting state of the animation.
<motion.div initial={{ opacity: 0, scale: 0.5 }}>
The target state to animate to.
<motion.div animate={{ opacity: 1, scale: 1 }}>
The state to animate to when the component is removed.
<motion.div exit={{ opacity: 0, scale: 0.5 }}>
Configure animation timing and easing.
<motion.div 
  transition={{ duration: 0.3, ease: "easeOut" }}
>

Dynamic Animations

import { motion } from 'framer-motion';

export default function ChallengeItem({ isExpanded, onViewDetails }) {
  return (
    <div className="challenge-item-details">
      <button onClick={onViewDetails}>
        View Details{' '}
        <motion.span
          animate={{ rotate: isExpanded ? 180 : 0 }}
          className="challenge-item-details-icon"
        >
          &#9650;
        </motion.span>
      </button>
    </div>
  );
}
Framer Motion automatically handles animations when prop values change. No need to manually trigger transitions!

Exit Animations

Animating component removal requires special handling since React removes elements immediately.

Using AnimatePresence

import { AnimatePresence } from 'framer-motion';
import Modal from './Modal';

export default function NewChallenge() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsModalOpen(true)}>
        New Challenge
      </button>

      <AnimatePresence>
        {isModalOpen && (
          <Modal
            title="New Challenge"
            onClose={() => setIsModalOpen(false)}
          >
            <form>...</form>
          </Modal>
        )}
      </AnimatePresence>
    </>
  );
}
AnimatePresence must be a direct parent of the conditionally rendered component. Each child must have a unique key prop if you’re rendering multiple animating components.

AnimatePresence with Lists

import { AnimatePresence, motion } from 'framer-motion';

export default function Challenges({ challenges }) {
  return (
    <ul className="challenge-items">
      <AnimatePresence>
        {challenges.map((challenge) => (
          <motion.li
            key={challenge.id}
            initial={{ opacity: 0, y: -20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: 20 }}
          >
            <ChallengeItem challenge={challenge} />
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  );
}

Animation Variants

Variants let you define reusable animation configurations.
import { motion } from 'framer-motion';

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { 
    opacity: 1, 
    y: 0,
    transition: { duration: 0.3 }
  },
  exit: { 
    opacity: 0, 
    y: -20,
    transition: { duration: 0.2 }
  }
};

export default function ChallengeItem({ challenge }) {
  return (
    <motion.article
      variants={itemVariants}
      initial="hidden"
      animate="visible"
      exit="exit"
      className="challenge-item"
    >
      <h2>{challenge.title}</h2>
      <p>{challenge.description}</p>
    </motion.article>
  );
}
Variants make your animations more maintainable and enable powerful features like animation orchestration and propagation to children.

Staggered Animations

Create cascading animations where child elements animate in sequence.
import { motion } from 'framer-motion';

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1
    }
  }
};

const itemVariants = {
  hidden: { opacity: 0, x: -20 },
  visible: { opacity: 1, x: 0 }
};

export default function ChallengeList({ challenges }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="challenge-items"
    >
      {challenges.map((challenge) => (
        <motion.li key={challenge.id} variants={itemVariants}>
          <ChallengeItem challenge={challenge} />
        </motion.li>
      ))}
    </motion.ul>
  );
}
The staggerChildren property automatically delays each child’s animation, creating a smooth cascade effect without manual timing calculations.

Layout Animations

Framer Motion can automatically animate layout changes.
import { motion } from 'framer-motion';

export default function ExpandablePanel({ isExpanded }) {
  return (
    <motion.div 
      layout
      className="panel"
    >
      <h3>Panel Title</h3>
      {isExpanded && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          <p>Panel content goes here...</p>
        </motion.div>
      )}
    </motion.div>
  );
}
The layout prop tells Framer Motion to animate any layout changes (position, size) automatically using the FLIP technique for optimal performance.

Imperative Animations

Use the useAnimation hook for programmatic control.
import { motion, useAnimation } from 'framer-motion';
import { useEffect } from 'react';

export default function Notification({ message, type }) {
  const controls = useAnimation();

  useEffect(() => {
    controls.start({
      opacity: [0, 1, 1, 0],
      y: [20, 0, 0, -20],
      transition: {
        duration: 3,
        times: [0, 0.1, 0.9, 1]
      }
    });
  }, [message, controls]);

  return (
    <motion.div
      animate={controls}
      className={`notification ${type}`}
    >
      {message}
    </motion.div>
  );
}

Gesture Animations

Framer Motion includes built-in gesture recognition.
import { motion } from 'framer-motion';

export default function Button({ children, onClick }) {
  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      className="button"
    >
      {children}
    </motion.button>
  );
}

Performance Optimization

These properties can be animated on the GPU for smooth 60fps animations.
// Good - GPU accelerated
<motion.div animate={{ x: 100, opacity: 0.5 }} />

// Avoid if possible - triggers layout recalculation
<motion.div animate={{ width: "100%", marginLeft: 20 }} />
The will-change CSS property can improve performance but use it carefully.
.animated-element {
  will-change: transform, opacity;
}
Respect user preferences for reduced motion.
import { useReducedMotion } from 'framer-motion';

function Component() {
  const shouldReduceMotion = useReducedMotion();
  
  return (
    <motion.div
      animate={{ x: shouldReduceMotion ? 0 : 100 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
    />
  );
}

Common Animation Patterns

Best Practices

Keep animations subtle

Animations should enhance UX, not distract. Use durations between 200-400ms for most UI transitions.

Be consistent

Use similar timing and easing throughout your app for a cohesive feel.

Consider performance

Stick to animating transform and opacity when possible for smooth 60fps animations.

Respect accessibility

Always honor the prefers-reduced-motion media query for users with motion sensitivities.

Choosing an Animation Approach

1

CSS Transitions

Best for: Simple hover effects, state changesPros: Performant, no JavaScript overhead, browser-optimizedCons: Limited control, no exit animations
2

CSS Keyframes

Best for: Complex multi-step animations, loading indicatorsPros: Full animation control, performant, works without JavaScriptCons: Can’t easily respond to React state changes
3

Framer Motion

Best for: Dynamic animations, gesture-based interactions, layout animationsPros: Declarative API, powerful features, great DXCons: Adds bundle size, requires JavaScript

Styling

Learn about styling approaches for your animated components

Refs & Portals

Combine animations with portals for modals and overlays

Build docs developers (and LLMs) love