React Transitions
React Transitions
What Are React Transitions?
In the React ecosystem, the word “transition” carries two distinct meanings that developers often conflate. Understanding both is the foundation of everything that follows.
A UI transition in React is any animated change in a component’s visual state, opacity, position, scale, colour, height that occurs over a defined duration, providing visual continuity as the interface updates.
A concurrent transition is a React 18 scheduling primitive (startTransition / useTransition) that marks certain state updates as non-urgent, allowing React to keep the UI responsive while expensive re-renders happen in the background.
Transitions are not the same as animations. A transition describes movement from state A to state B. An animation can loop, reverse, and run independently of state. React supports both, but transitions are the more common need.
How Transitions Fit into React’s Mental Model
React renders UI as a pure function of state: UI = f(state). Transitions are what make the journey between two states visible they answer the question “how does the UI move from state A to state B?” rather than just showing the final result instantly.
- State changes: an event fires (click, timer, API response) and updates component state.
- React re-renders: the component tree re-evaluates with the new state.
- DOM updates: React commits changes to the actual DOM.
- Transition runs: CSS, JS, or a library interpolates values between the old and new state over time.
Why Do Transitions Matter?
Transitions are not decoration. They serve concrete UX and performance purposes:
Core Benefits
- Orientation: spatial transitions (slide, fade-through) tell users where content came from and where it went, reducing disorientation.
- Perceived Performance: a 300ms fade-in feels faster than an instant render that stalls, because animation gives the brain something to process during load.
- Continuity: shared-element transitions (e.g. a card expanding into a detail view) maintain visual context, reducing cognitive load.
- Feedback: hover transitions, button press animations, and loading spinners signal that the UI has received user input.
- Professionalism: polished transitions distinguish production-grade applications from prototypes.
prefers-reduced-motion media query. Users with vestibular disorders can experience nausea from excessive animation. Wrap all non-essential transitions in a reduced-motion check.CSSTransition Props Reference
| Prop | Type | Description |
|---|---|---|
in |
boolean |
Triggers enter transition when true, and exit transition when false. |
timeout |
number | object |
Duration of the transition in milliseconds. Can be a single number or separate values like { enter: 300, exit: 200 }. |
classNames |
string | object |
Prefix string or object defining transition class names such as enter, enterActive, exit, and exitActive. |
unmountOnExit |
boolean |
Removes the component from the DOM after the exit transition completes. |
mountOnEnter |
boolean |
Delays mounting the component until the enter transition starts. |
appear |
boolean |
Runs the enter transition when the component mounts for the first time. |
onEnter / onExit |
function |
Lifecycle callback functions triggered during transition stages like enter, entering, entered, exit, exiting, and exited. |
Best Practices
Performance
- Only animate
opacityandtransform. These are the only properties browsers can animate on the GPU compositor thread without triggering layout or paint. Animatingwidth,height,top, orbackground-colorcauses layout thrashing. - Use
will-change: transformsparingly. It promotes the element to its own compositor layer. Overuse increases VRAM consumption. Apply only to elements that transition frequently. - Avoid long transition chains. Keep total animation duration under 500ms for UI transitions. Interactions should feel instant — longer times feel sluggish.
- Use
unmountOnExitin react-transition-group (orAnimatePresencein Framer Motion) to remove hidden elements from the DOM and reduce render overhead.
Accessibility
ReducedMotion.jsx
JSX
import { useReducedMotion } from 'framer-motion'; export function AccessibleFade({ children }) { const prefersReduced = useReducedMotion(); return ( <motion.div initial={{ opacity: 0, y: prefersReduced ? 0 : 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: prefersReduced ? 0.01 : 0.4 }} > {children} </motion.div> ); } /* Or in pure CSS: */ /* @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } */
Easing Guidelines
| Use Case | Recommended Easing | Duration |
|---|---|---|
| Element entering screen | ease-out / cubic-bezier(0,0,0.2,1) |
200–400ms |
| Element leaving screen | ease-in / cubic-bezier(0.4,0,1,1) |
150–250ms |
| Moving between two positions | ease-in-out / cubic-bezier(0.4,0,0.2,1) |
200–350ms |
| Spring / bounce feel | Spring: stiffness 400, damping 30 |
Physics-based |
| Micro-interactions (button) | ease or linear |
80–150ms |
Library Comparison
Choosing the right tool depends on your project’s complexity, bundle size tolerance, and animation needs.
| Library / Approach | Bundle Size | Mount/Unmount | Physics | Layout Anim. | Learning Curve |
|---|---|---|---|---|---|
| CSS Transitions | 0 KB | ✗ | ✗ | ✗ | Low |
| react-transition-group | ~20 KB | ✓ | ✗ | ✗ | Medium |
| Framer Motion | ~50 KB | ✓ | ✓ | ✓ | Low–Medium |
| GSAP + @gsap/react | ~30 KB | ✓ | Plugin | Plugin | High |
| React Spring | ~45 KB | ✓ | ✓ | ✗ | Medium |
| useTransition (React 18) | 0 KB (built-in) | ✗ | ✗ | ✗ | Low |
When to Use Which
- CSS Transitions: hover states, toggles, colour changes. No JS overhead needed.
- react-transition-group: modal show/hide, route changes when you already use CSS for styles.
- Framer Motion: complex UI animations, shared-element transitions, drag gestures, layout animations. Best for most React projects.
- GSAP: advanced timelines, scroll-triggered animations, SVG morphing. When Framer Motion isn’t enough.
- React Spring: physics-based motion without Framer Motion, good for data visualisation.
- useTransition: any expensive re-render (large lists, heavy computations) that blocks user input.
Frequently Asked Questions
What is the difference between a transition and an animation in React?
A transition interpolates between two known states (A → B). It has a clear beginning and end, and is triggered by a state change. A CSS transition property is the classic example.
An animation (CSS @keyframes, Framer Motion’s animate sequences) can be multi-step, loop, reverse, and run independently of component state. The two concepts overlap in libraries like Framer Motion, which unifies them under a single API.
Why can’t CSS transitions animate unmounting components?
When React unmounts a component, it removes the DOM node immediately. CSS transitions rely on the DOM node existing to interpolate from one state to another but if the node is gone, there’s nothing to transition.
Libraries like react-transition-group and Framer Motion’s AnimatePresence solve this by delaying the unmount until the exit transition completes, keeping the node in the DOM long enough for the animation to run.
Is Framer Motion worth the bundle size for small projects?
For simple hover transitions and colour changes, pure CSS is always preferable — zero JS, hardware-accelerated, accessible by default.
Framer Motion (~50 KB gzipped) earns its size when you need: mount/unmount animations, layout animations, spring physics, drag gestures, or shared-element transitions. For a marketing site or small app with only basic transitions, react-transition-group (~20 KB) combined with CSS is a lighter alternative.
What does useTransition actually do under the hood?
useTransition wraps a state update in React’s concurrent scheduler as a low-priority update. React 18’s scheduler can interrupt and deprioritize this work if a higher-priority update (like a keypress) arrives.
Under the hood, React renders the transitioned state in the background (without committing it to the DOM) while displaying the current state. If the user types again before the transition finishes, React throws away the in-progress work and starts fresh. This is the fundamental property that prevents UI jank.
How do I animate a list when items are added or removed?
Use <TransitionGroup> + <CSSTransition> from react-transition-group, or <AnimatePresence> with Framer Motion. Both manage tracking which items are entering and which are exiting, even when multiple changes happen simultaneously.
Always provide a stable, unique key prop based on item identity (not array index) — this is what both libraries use to track individual items across re-renders.
How do I implement page transitions with React Router?
Wrap your <Routes> in Framer Motion’s <AnimatePresence mode="wait"> and pass location + key={location.pathname} to <Routes>. Wrap each route’s element in a <motion.div> with initial, animate, and exit props.
The mode="wait" prop ensures the exiting page fully completes its exit animation before the entering page begins — preventing two pages from being visible simultaneously.
How do I respect prefers-reduced-motion?
In CSS: use a @media (prefers-reduced-motion: reduce) block to override or disable transitions. In Framer Motion: use the useReducedMotion() hook to conditionally disable or simplify animations. In react-transition-group: set timeout={0} when the hook returns true.
WCAG 2.1 Success Criterion 2.3.3 (AAA) requires that users can disable motion. At minimum, provide a CSS-level override. It’s a five-line addition that massively improves accessibility.
Can I use multiple animation libraries in the same project?
Yes, though try to standardise where possible to avoid bloat. A common pattern is using CSS transitions for simple stateful changes, Framer Motion for complex component animations, and useTransition for expensive state updates — each handling a distinct concern without conflict.
Avoid using both react-spring and Framer Motion for the same use case — they overlap heavily and the combined bundle size is rarely justified.
What is the difference between useDeferredValue and useTransition?
useTransition: you own the state update. You call startTransition(() => setState(...)) to mark it as non-urgent. Returns an isPending boolean.
useDeferredValue: you receive a prop or value from outside that you can’t control. You pass it through useDeferredValue(prop) to get a deferred copy that lags behind during busy renders. No isPending flag — check by comparing the original and deferred values instead (query !== deferredQuery).
Why do my animations run twice in React 18 Strict Mode?
React 18 Strict Mode intentionally double-invokes effects (useEffect) in development to surface side-effect bugs. If your animation is triggered inside a useEffect, it will run twice in development, once in production.
Solutions: (1) use CSS @keyframes triggered by class names — these aren’t affected by Strict Mode. (2) Use Framer Motion’s declarative initial/animate API, which handles remounting cleanly. (3) Return a cleanup function from useEffect that resets your animation state.
