React Transitions and Animations: A Practical, High‑Performance Guide
A practical guide to smooth animations in React using CSS, libraries, and useTransition—covering patterns, performance, accessibility, and testing.
Image used for representation purposes only.
Overview
Smooth, meaningful motion can turn a good React app into a great one. This guide walks through the practical ways to build transitions and animations in React—ranging from CSS transitions to full-featured libraries—while explaining how React’s concurrent features like useTransition and startTransition help keep animations responsive during heavy state updates.
What you’ll learn:
- When to use CSS, the Web Animations API, or a library
- Mount/unmount transitions and route/page transitions
- Coordinating animations with React state using useTransition
- Performance, accessibility, and testing best practices
The core options at a glance
- CSS transitions/animations: Small, declarative, zero‑dependency. Great for simple hover, fade, and slide effects.
- Web Animations API (WAAPI): Imperative, keyframe‑driven animations in JavaScript with good performance and timing control.
- Libraries:
- React Transition Group (RTG): Minimal, “bring your own CSS,” focused on mount/unmount and list transitions.
- Framer Motion: High-level, batteries-included, expressive API with layout and exit animations.
- react-spring: Physics‑based springs for natural motion; excels at dynamic, data‑driven transitions.
- React concurrent transitions: useTransition/startTransition help avoid UI stalls while animations run alongside expensive renders.
A simple rule of thumb:
- Prefer CSS for simple effects.
- Use Framer Motion or RTG for page/route and list enter/exit.
- Reach for react-spring for physics and continuous, data‑driven motion.
- Consider WAAPI when you need low-level control or to avoid extra deps.
CSS transitions: the baseline
CSS remains the lightest way to animate in React—toggle classes or inline styles from state.
import { useState } from 'react';
import './panel.css';
export default function SlidingPanel() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(o => !o)}>{open ? 'Close' : 'Open'} Panel</button>
<div className={`panel ${open ? 'panel--open' : ''}`}>Hello</div>
</div>
);
}
/* panel.css */
.panel {
transform: translateY(-8px);
opacity: 0;
transition: transform 200ms ease, opacity 200ms ease;
}
.panel--open {
transform: translateY(0);
opacity: 1;
}
Pros: tiny, fast, simple. Cons: coordinating enter/exit on unmount is tricky without helpers.
Mount/unmount with React Transition Group
React Transition Group gives lifecycle hooks for when elements enter and leave the DOM.
npm i react-transition-group
import { CSSTransition, TransitionGroup } from 'react-transition-group';
function TodoList({ items, onRemove }) {
return (
<TransitionGroup component="ul" className="todos">
{items.map(item => (
<CSSTransition key={item.id} classNames="fade" timeout={200}>
<li>
{item.text}
<button onClick={() => onRemove(item.id)}>✕</button>
</li>
</CSSTransition>
))}
</TransitionGroup>
);
}
.fade-enter { opacity: 0; transform: translateY(-4px); }
.fade-enter-active { opacity: 1; transform: translateY(0); transition: all 200ms ease; }
.fade-exit { opacity: 1; }
.fade-exit-active { opacity: 0; transform: translateY(-4px); transition: all 150ms ease; }
Why it’s great: It delays unmount until the exit animation finishes, and it scales to lists.
Framer Motion: expressive, layout‑aware animations
Framer Motion provides declarative props and exit animations via AnimatePresence.
npm i framer-motion
import { motion, AnimatePresence } from 'framer-motion';
export default function Modal({ open, onClose }) {
return (
<AnimatePresence>
{open && (
<motion.div
className="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="modal"
initial={{ y: 24, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 24, opacity: 0 }}
transition={{ type: 'spring', stiffness: 500, damping: 35 }}
>
<button onClick={onClose}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
Highlights:
- AnimatePresence enables exit animations before unmount.
- layout and layoutId create FLIP-style layout transitions across components.
- Variants coordinate complex, multi-element sequences.
Physics with react-spring
react-spring models motion with springs—ideal for natural-feel UI and list transitions.
npm i react-spring
import { useTransition, animated } from '@react-spring/web';
function Chips({ items }) {
const transitions = useTransition(items, {
keys: item => item.id,
from: { opacity: 0, transform: 'scale(0.9)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.9)' },
trail: 40
});
return transitions((style, item) => (
<animated.span style={style} className="chip">{item.label}</animated.span>
));
}
Web Animations API (no library needed)
WAAPI is imperative and great for fine-grained control and timelines.
import { useEffect, useRef } from 'react';
function Pulse() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
const anim = el.animate(
[ { transform: 'scale(1)' }, { transform: 'scale(1.06)' }, { transform: 'scale(1)' } ],
{ duration: 600, easing: 'ease-in-out', iterations: Infinity }
);
return () => anim.cancel();
}, []);
return <button ref={ref} className="cta">Buy now</button>;
}
Pros: great performance, cancelable, timeline control. Cons: more code, less declarative.
Keeping animations smooth with React’s useTransition
Animations can stutter when state updates trigger expensive renders. React’s concurrent features help prioritize what’s urgent.
- useTransition: Mark certain updates as “transition” work. React will keep urgent interactions (like typing) responsive while deferring non-urgent updates.
- startTransition: Imperative API to wrap non-urgent setState calls.
import { useState, useTransition, startTransition } from 'react';
function Search({ records }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState(records);
const [isPending, start] = useTransition();
function onChange(e) {
const q = e.target.value;
setQuery(q); // urgent: reflect keystroke immediately
start(() => {
// non-urgent: expensive filtering and list diffing
const next = records.filter(r => r.name.includes(q));
setResults(next);
});
}
return (
<div>
<input value={query} onChange={onChange} placeholder="Filter" />
{isPending && <span className="dim">Updating…</span>}
<LargeAnimatedList items={results} />
</div>
);
}
Guidelines:
- Wrap only the expensive, non-urgent updates.
- Pair with a visual pending state for feedback.
- Works well with animated lists (Framer Motion/RTG) to keep 60fps feel while data recalculates.
Layout transitions (FLIP) and measurement
When elements change size/position, aim for transform/opacity rather than layout-affecting properties.
FLIP technique:
- First: read current position
- Last: render to final position
- Invert: apply transform to counter the delta
- Play: animate transform back to zero
Framer Motion’s layout/layoutId automates this. For custom implementations, measure in useLayoutEffect and use transforms.
import { useLayoutEffect, useRef, useState } from 'react';
function FlipCard({ expanded }) {
const ref = useRef(null);
useLayoutEffect(() => {
const el = ref.current;
const first = el.getBoundingClientRect();
// render to final state synchronously
// (in React 18+, you can force a sync flush if needed)
}, [expanded]);
return <div ref={ref} className={expanded ? 'card card--open' : 'card'} />;
}
Tip: If precise measurement is required immediately after a state change, use useLayoutEffect to avoid visible jumps.
Route and page transitions
For React Router, wrap routes in AnimatePresence to animate navigation and exits.
import { Routes, Route, useLocation } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
function AppRoutes() {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
<Route path="/" element={<Page>Home</Page>} />
<Route path="/about" element={<Page>About</Page>} />
</Routes>
</AnimatePresence>
);
}
function Page({ children }) {
return (
<motion.main
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.2 }}
>
{children}
</motion.main>
);
}
Accessibility: motion that respects users
- honors prefers-reduced-motion: reduce or disable animations for users who opt out.
- Avoid parallax/auto-scrolling for vestibular safety.
- Keep focus management intact during route transitions (return focus to meaningful targets).
@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;
}
}
In component libraries (e.g., modals), consider a noMotion prop to disable motion programmatically when needed.
Performance checklist
- Animate transform and opacity; avoid layout-triggering properties (top/left/width/height) when possible.
- Prefer GPU-friendly 2D transforms; be conservative with will-change to avoid memory bloat.
- Batch state updates and use memoization for large lists.
- Virtualize long lists; don’t try to animate thousands of DOM nodes at once.
- Keep images and fonts loaded before animating; use content-visibility and lazy loading.
- Measure: use Chrome DevTools Performance panel and FPS meter; profile both JS and rendering.
Coordinating data fetching and animation
When a transition also loads data, show immediate lightweight motion (skeletal shimmer or scale-in) while deferring heavy list renders via useTransition. This keeps the UI responsive and avoids jank.
function Products() {
const [isPending, start] = useTransition();
const [category, setCategory] = useState('all');
const [items, setItems] = useState([]);
async function changeCategory(next) {
setCategory(next); // urgent: reflect selection
start(async () => {
const data = await fetch(`/api/products?cat=${next}`).then(r => r.json());
setItems(data);
});
}
return (
<div>
<Tabs value={category} onChange={changeCategory} />
<GridSkeleton visible={isPending} />
<AnimatedGrid items={items} />
</div>
);
}
Testing animations
- Prefer asserting end states rather than timing. Use findBy* queries to wait for DOM after animation completes.
- In unit tests, reduce durations to 0 or mock animation frames.
- For accessibility, include snapshots with prefers-reduced-motion enabled to ensure no blocking animations remain.
// Example with Jest + Testing Library
jest.mock('framer-motion', () => ({
...jest.requireActual('framer-motion'),
motion: (c) => c, // shallow out motion wrappers for logic tests
}));
Common pitfalls and fixes
- Unmounting before exit animation finishes: use RTG or AnimatePresence.
- Jank during expensive renders: wrap non-urgent work in useTransition/startTransition.
- Measuring layout after render causing flicker: move to useLayoutEffect; avoid reading layout mid-animation.
- Animating large images/SVG filters: pre-rasterize if possible, simplify SVGs, or avoid animating filters.
- Conflicting CSS and JS animations: keep a single source of truth per property.
Choosing the right tool: a quick decision guide
- “I only need simple fades/slides.” → CSS transitions.
- “I want reliable enter/exit and list item transitions with my own CSS.” → React Transition Group.
- “I want expressive, production‑ready motion with layout/exit and variants.” → Framer Motion.
- “I need physics‑based, data‑driven motion.” → react-spring.
- “I need low-level timeline control without a dependency.” → Web Animations API.
- “My animations stutter when data updates.” → Combine your choice above with useTransition.
Final thoughts
React doesn’t prescribe a single animation solution—and that’s a strength. Start simple with CSS, adopt a library where it compounds your velocity and quality, and use React’s concurrent transitions to keep everything responsive. With the right tool for the job and a focus on accessibility and performance, your UI can feel fast, fluid, and polished.
Related Posts
Framer Motion for React: A Practical Tutorial with Patterns and Pitfalls
Hands-on Framer Motion tutorial for React: setup, key APIs, gestures, layout, AnimatePresence, scroll, SVG, performance, TypeScript, testing.
Choosing and Using a React Table Sorting & Filtering Library: A 2026 Guide
A 2026 guide to React table sorting and filtering libraries: compare options, implement TanStack Table, and nail performance, a11y, and testing.
React Component Composition Patterns: A Practical Guide
Learn modern React composition patterns—children, render props, compound components, headless hooks, and more—with examples and design tips.