React: Transitions and Animations

Last updated: 2026-08-26

Tom had just finished a data dashboard project, but the product manager pointed out that the page transitions were too abrupt—the data appeared with a “flash” when the list loaded, and pop-ups opened and closed instantly. Tom realized that having all the features doesn’t necessarily mean a good user experience. He needed to add smooth transition animations to the pages.


1. What You'll Learn



2. Conceptual Diagrams

The following diagram will help you make quick decisions when choosing an animation option:

100%
flowchart TD
    A[Animation effects are needed] --> B{Effect Complexity}
    B -->|Simple Transition| C[CSS Transition]
    B -->|Keyframe Animation| D[CSS Animation]
    B -->|Complex Interactions| E[Framer Motion]

    C --> C1["hover / focus Effect"]
    C --> C2["opacity / transform Fade Animation"]
    C --> C3["Changes to a Single Attribute"]

    D --> D1["@keyframes Defining Multi-Stage"]
    D --> D2["Infinite Loop Animation"]
    D --> D3["Loading the skeleton screen"]

    E --> E1["mount/unmount Animation"]
    E --> E2["Drag / Gestures"]
    E --> E3["List layout Animation"]
    E --> E4["Spring Physical Animation Effects"]

    style C fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32
    style E fill:#fff3e0,stroke:#e65100


3. A Real-Life Scenario

Tom's dashboard includes three core interactions: task list loading, modal window toggling, and card dragging and sorting. His goal:

  1. When the task list loads, each task slides in from the left.
  2. When the pop-up opens, the background fades in and the pop-up zooms in; when it closes, the animation runs in reverse.
  3. The card follows your finger when you drag it and snaps back into place when you let go.

He started with the simplest CSS transitions and gradually moved on to creating complex animations using Framer Motion.


(1) CSS Transitions — The Simplest Progressive Enhancement

CSS Transitions are used to create smooth transitions between the initial state and the final state. They do not require any JavaScript libraries and take effect simply by declaring them in CSS.

The Four Core Subproperties of Transition

CSS
/* transition: property duration timing-function delay */
.example {
  transition: opacity 0.3s ease 0s;
  /* Complete Syntax:
     transition-property: opacity
     transition-duration: 0.3s
     transition-timing-function: ease
     transition-delay: 0s
  */
}

Common timing-function values:

▶ Example 1: Implementing an Accordion Component Using CSS Transitions

Output:

TEXT 📖 Display only
Styled components with CSS-in-JS or scoped styles. Animated transitions: elements fade/slide/move smoothly
JSX
import { useState } from 'react'

const accordionData = [
  { title: 'What is React?', content: 'React It is a tool for building user interfaces. JavaScript library.' },
  { title: 'What Is a Component??', content: 'A component is React The smallest independent unit of an application,Reusable and combinable。' },
  { title: 'What is State?', content: 'State It is variable data within the component,Driver UI Update。' },
]

function Accordion() {
  const [openIndex, setOpenIndex] = useState(null)

  function toggle(index) {
    setOpenIndex(openIndex === index ? null : index)
  }

  return (
    <div style={{ maxWidth: 500, fontFamily: 'sans-serif' }}>
      {accordionData.map((item, index) => (
        <div
          key={index}
          style={{
            border: '1px solid #e8e8e8',
            borderRadius: 8,
            marginBottom: 8,
            overflow: 'hidden',
          }}
        >
          <button
            onClick={() => toggle(index)}
            style={{
              width: '100%',
              padding: '12px 16px',
              background: openIndex === index ? '#e6f7ff' : '#fafafa',
              border: 'none',
              cursor: 'pointer',
              fontSize: 15,
              fontWeight: 600,
              textAlign: 'left',
              transition: 'background 0.2s ease',
            }}
          >
            {item.title}
          </button>
          <div
            style={{
              maxHeight: openIndex === index ? 80 : 0,
              padding: openIndex === index ? '12px 16px' : '0 16px',
              opacity: openIndex === index ? 1 : 0,
              transition: 'all 0.3s ease',
              color: '#666',
              fontSize: 14,
              lineHeight: 1.6,
            }}
          >
            {item.content}
          </div>
        </div>
      ))}
    </div>
  )
}

Output:

TEXT 📖 Display only
Fade in/out: opacity 0→1 over 300ms. Toggle button shows/hides element with smooth CSS transition.

Key Point: maxHeight + opacity + padding transition simultaneously to achieve the expand/collapse effect for the content area. Note that the value of maxHeight should be slightly greater than the actual content height.


(2) CSS Animation — Keyframe-Driven Animation

When an animation has more than two stages (beyond just start → end) or needs to loop, CSS Animation is the better choice. It uses @keyframes to define multi-stage animation sequences.

Core Attributes

CSS
/* animation: name duration timing-function delay iteration-count direction fill-mode */
.pulse {
  animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse {
  0%   { transform: scale(1); opacity: 1; }
  50%  { transform: scale(1.05); opacity: 0.7; }
  100% { transform: scale(1); opacity: 1; }
}

▶ Example 2: Skeleton Screen Load Animation

Output:

TEXT 📖 Display only
Skeleton screen: gray animated placeholders while loading → replaced by real content on fetch complete
JSX
import './Skeleton.css'

function Skeleton({ count = 3 }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      {Array.from({ length: count }).map((_, i) => (
        <div key={i} className="skeleton-row">
          <div className="skeleton-avatar" />
          <div style={{ flex: 1 }}>
            <div className="skeleton-line" style={{ width: '60%' }} />
            <div className="skeleton-line" style={{ width: '90%', marginTop: 8 }} />
          </div>
        </div>
      ))}
    </div>
  )
}

export default Skeleton
▶ Try it Yourself

Output:

TEXT 📖 Display only
Skeleton screen: gray placeholder blocks pulse/shimmer while data loads → replaces with real content when fetch completes
CSS
/* Skeleton.css */
.skeleton-row {
  display: flex;
  gap: 12px;
  align-items: center;
}

.skeleton-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
}

.skeleton-line {
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
}

@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

How Skeleton Screen Animations Work: The movement of background-position simulates the visual effect of a light trail sweeping across the screen during loading. The infinite property in CSS Animation causes the light trail to loop continuously until the actual content finishes loading and replaces the skeleton screen.


(3) Framer Motion — Complex React Animations

Framer Motion is the most powerful animation library in the React ecosystem, offering three core capabilities:

Capability Problem Solved Corresponding API
Enter/Exit Animations Animations When Components Are Mounted/Unmounted motion.div + AnimatePresence
Layout Animation Smooth Transitions When List Order Changes layout prop
Gesture Animations Drag, Hover, and Tap Gestures drag / whileHover / whileTap

Installation

BASH
npm install framer-motion

▶ Example 3: Implementing a To-Do List with Framer Motion

Output:

TEXT 📖 Display only
Todo list with Framer Motion: animated add/remove/reorder. Click to toggle done (strikethrough). Drag to reorder items
JSX
import { useState } from 'react'
import { motion, AnimatePresence, Reorder } from 'framer-motion'

const initialTodos = [
  { id: 1, text: 'Done React Learning Animation', done: false },
  { id: 2, text: 'Reworking the Dashboard Components', done: false },
  { id: 3, text: 'Code Review PR #142', done: false },
  { id: 4, text: 'Update Project Documentation', done: true },
  { id: 5, text: 'Fix the Responsive Navigation BarBUG', done: false },
]

function TodoList() {
  const [todos, setTodos] = useState(initialTodos)

  function toggleDone(id) {
    setTodos(prev =>
      prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
    )
  }

  function removeTodo(id) {
    setTodos(prev => prev.filter(t => t.id !== id))
  }

  return (
    <div style={{ maxWidth: 480, fontFamily: 'sans-serif' }}>
      <AnimatePresence>
        {todos.map(todo => (
          <motion.div
            key={todo.id}
            layout
            initial={{ opacity: 0, x: -60 }}
            animate={{
              opacity: 1,
              x: 0,
              background: todo.done ? '#f6ffed' : '#fff',
            }}
            exit={{ opacity: 0, x: 100, height: 0, marginBottom: 0 }}
            transition={{ type: 'spring', stiffness: 300, damping: 25 }}
            style={{
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'space-between',
              padding: '12px 16px',
              marginBottom: 8,
              borderRadius: 8,
              border: '1px solid #f0f0f0',
              cursor: 'pointer',
            }}
            whileHover={{ scale: 1.02 }}
            whileTap={{ scale: 0.98 }}
          >
            <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => toggleDone(todo.id)}
                style={{ width: 18, height: 18, cursor: 'pointer' }}
              />
              <span
                style={{
                  textDecoration: todo.done ? 'line-through' : 'none',
                  color: todo.done ? '#999' : '#333',
                  fontSize: 15,
                }}
              >
                {todo.text}
              </span>
            </div>
            <button
              onClick={() => removeTodo(todo.id)}
              style={{
                border: 'none',
                background: 'none',
                color: '#ff4d4f',
                cursor: 'pointer',
                fontSize: 18,
                fontWeight: 'bold',
              }}
            >
              x
            </button>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  )
}

export default TodoList

Output:

TEXT 📖 Display only
Fade in/out: opacity 0→1 over 300ms. Toggle button shows/hides element with smooth CSS transition.

This code demonstrates four key features of Framer Motion:

  1. initial / animate / exit: Define the component’s entry, display, and exit states. AnimatePresence allows conditional rendering to trigger the exit animation as well.
  2. layout prop: When a list item changes position due to a change in the done state, it automatically transitions smoothly to the new position without the need to calculate coordinates.
  3. transition: Uses the spring physical spring animation; stiffness controls spring stiffness; damping controls damping (the higher the value, the less springy it is)
  4. whileHover / whileTap: Interactive gesture animation; zooms in to 1.02x when the mouse hovers over it, and zooms out to 0.98x when clicked

▶ Example 4: Framer Motion Modal Pop-up Animation

Output:

TEXT 📖 Display only
Framer Motion: animated enter/exit/layout transitions. AnimatePresence for mount/unmount animations
JSX
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'

function Modal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          key="overlay"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.2 }}
          style={{
            position: 'fixed',
            inset: 0,
            background: 'rgba(0,0,0,0.45)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            zIndex: 1000,
          }}
          onClick={onClose}
        >
          <motion.div
            key="modal"
            initial={{ scale: 0.85, opacity: 0, y: 20 }}
            animate={{ scale: 1, opacity: 1, y: 0 }}
            exit={{ scale: 0.85, opacity: 0, y: 20 }}
            transition={{ type: 'spring', stiffness: 400, damping: 30 }}
            style={{
              background: '#fff',
              padding: 24,
              borderRadius: 12,
              minWidth: 320,
              boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
              position: 'relative',
            }}
            onClick={e => e.stopPropagation()}
          >
            {children}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

function App() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button
        onClick={() => setIsOpen(true)}
        style={{ padding: '8px 20px', fontSize: 15, cursor: 'pointer' }}
      >
        Open the pop-up window
      </button>

      <Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
        <h2 style={{ margin: '0 0 12px' }}>Confirm Action</h2>
        <p style={{ color: '#666', marginBottom: 20 }}>
          Are you sure you want to delete this record??This action cannot be undone.。
        </p>
        <div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
          <button
            onClick={() => setIsOpen(false)}
            style={{ padding: '8px 20px', cursor: 'pointer' }}
          >
            Cancel
          </button>
          <button
            onClick={() => {
              alert('Deleted')
              setIsOpen(false)
            }}
            style={{
              padding: '8px 20px',
              background: '#ff4d4f',
              color: '#fff',
              border: 'none',
              borderRadius: 6,
              cursor: 'pointer',
            }}
          >
            Confirm Deletion
          </button>
        </div>
      </Modal>
    </div>
  )
}

Output:

TEXT 📖 Display only
Fade in/out: opacity 0→1 over 300ms. Toggle button shows/hides element with smooth CSS transition.

Key Points for Designing Pop-up Animations:



4. Best Practices for Animation Performance

Attribute Trigger Phase Performance Recommendation
transform Composite ✅ Best ⭐⭐⭐
opacity Composite ✅ Best ⭐⭐⭐
filter Paint ⚠️ Medium ⭐⭐
box-shadow Paint ⚠️ Medium
width/height Layout ❌ Expensive
top/left/margin Layout ❌ Expensive
Rule Description
Use transform and opacity by default These properties trigger the compositor thread without causing a Layout/Paint
Avoid animations width / height / top / left These properties trigger a layout reflow, which is resource-intensive
Use will-change to prompt the browser will-change: transform, opacity to create a composite layer in advance
Avoid animating too many elements at once Animating more than 20 elements simultaneously may cause frame drops
Using requestAnimationFrame Avoiding the Use of setTimeout in JavaScript-Driven Animations

▶ Example 5: CSS Keyframe Animation—Loading Skeleton Screen

Output:

TEXT 📖 Display only
Skeleton screen: gray animated placeholders while loading → replaced by real content on fetch complete
JSX
function SkeletonCard() {
  return (
    <div style={{ border: '1px solid #f0f0f0', borderRadius: 8, padding: 16, overflow: 'hidden' }}>
      <div style={{
        height: 120, borderRadius: 4, marginBottom: 12,
        background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
        backgroundSize: '200% 100%',
        animation: 'shimmer 1.5s infinite',
      }} />
      <div style={{
        height: 16, width: '60%', borderRadius: 4, marginBottom: 8,
        background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
        backgroundSize: '200% 100%',
        animation: 'shimmer 1.5s infinite',
      }} />
      <div style={{
        height: 12, width: '40%', borderRadius: 4,
        background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
        backgroundSize: '200% 100%',
        animation: 'shimmer 1.5s infinite',
      }} />
      <style>{`
        @keyframes shimmer {
          0% { background-position: 200% 0; }
          100% { background-position: -200% 0; }
        }
      `}</style>
    </div>
  )
}

function LoadingGrid() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, maxWidth: 800, margin: '0 auto' }}>
      {[1, 2, 3].map(i => <SkeletonCard key={i} />)}
    </div>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Infinite loop: setState in useEffect without deps → render → effect → setState → render... Fix: add deps or use empty array [].

❓ FAQ

Q What exactly is the difference between CSS Transitions and CSS Animations?
A A transition is a two-stage transition "from A to B" triggered by a state change (such as a hover) and does not support looping. An animation is a multi-stage keyframe animation that can loop indefinitely, play in reverse, and be paused or resumed. Use transitions for simple scenarios and animations for complex sequences.
Q Why does AnimatePresence in Framer Motion sometimes not work?
A The most common reason is that key is not set up correctly. AnimatePresence tracks child elements via key; if the key remains unchanged, it interprets the element as "updated" rather than "removed," and the exit animation will not trigger. Additionally, AnimatePresence’s direct child elements must be motion components.
Q How can animation performance be ensured on mobile devices?
A Stick to using only transform and opacity animations, as these properties run on the mobile GPU compositing layer. Framer Motion uses transform by default, which ensures good performance. If you must create complex animations on mobile devices, you can use the useWillChange hook or manually set will-change.
Q What should I do if animations cause layout shifts?
A Reserve a fixed size for animated elements to prevent the layout from changing during the animation. In Framer Motion, the layout prop can be used in combination with layoutId and AnimatePresence to achieve a stable layout transition. If using CSS, consider using transform: translateX() instead of left for position animations.
Q What is the difference between React 18’s useTransition and Framer Motion?
A useTransition is a built-in concurrency feature in React that marks certain state updates as “non-urgent” (such as filtering search results), allowing urgent updates (such as typing in an input field) to be processed first to prevent stuttering. Framer Motion is an animation library that handles visual effects such as movement, scaling, and opacity for UI elements. The two address different issues: useTransition addresses “interaction lag,” while Framer Motion addresses “visual transitions.”

📖 Summary


📝 Exercises

  1. Use CSS Transitions to implement a "Back to Top" button: It fades in when the page is scrolled more than 300px; when clicked, it smoothly scrolls to the top; and it fades out and hides once it reaches the top.
  2. Use CSS Animation to create a loading spinner: a rotating circle animation. Use @keyframes to control the rotation from 0deg to 360deg, and use linear for the speed curve to maintain a constant speed.
  3. Use Framer Motion to create a draggable floating action button (FAB). The drag prop allows it to be dragged anywhere on the screen, and when dragging ends, use the spring animation to snap it to the nearest screen edge.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏