React / React Transitions
Home /React /React Transitions

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.

UI Transitions

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.

Concurrent Transitions (React 18)

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.
Motion sickness warning: Always respect the 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 opacity and transform. These are the only properties browsers can animate on the GPU compositor thread without triggering layout or paint. Animating width, height, top, or background-color causes layout thrashing.
  • Use will-change: transform sparingly. 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 unmountOnExit in react-transition-group (or AnimatePresence in 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.