DEV Community

Cover image for A Practical Guide to React's New <ViewTransition>: What Actually Animates (and What Doesn't)
Shrey Saraswat
Shrey Saraswat

Posted on

A Practical Guide to React's New <ViewTransition>: What Actually Animates (and What Doesn't)

React 19.3 shipped on npm on September 9, 2026, and it quietly stabilized something a lot of us have wanted for years: a native way to animate UI changes without reaching for a third-party library.

What <ViewTransition> actually is

It's a component that wraps part of your UI and tells React: "when this changes, animate it using the browser's native View Transition API instead of just swapping the DOM."

import { ViewTransition, useState, startTransition } from 'react';

function Panel() {
  const [showItem, setShowItem] = useState(false);

  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}
      >
        {showItem ? '' : ''}
      </button>

      {showItem && (
        <ViewTransition>
          <Details />
        </ViewTransition>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Toggle showItem, and instead of the panel just popping in and out, it cross-fades. No CSS animation library, no manual onExit juggling.

React decides which kind of animation to run based on how the tree changed:

  • enter — the <ViewTransition> gets added
  • exit — it gets removed
  • update — its children change style or content
  • share — a named moves from one place to another

The rule that trips people up

Here's the part most quick-recap posts gloss over: <ViewTransition> doesn't animate every update.

It only animates changes caused by:

  • startTransition
  • a <Suspense> reveal
  • useDeferredValue

A plain setState call inside a regular event handler won't trigger the animation — it just updates immediately, the same as always. This is deliberate. React treats "urgent" interactions (typing, dragging, anything that needs to feel instant) differently from "non-urgent" ones (navigating, revealing content), and only the latter get animated.

So if you wrap something in <ViewTransition> and nothing happens, the first thing to check isn't your CSS — it's whether the state update is actually wrapped in startTransition.

Customizing animations by cause: addTransitionType

A single piece of state can change for different reasons that should animate differently. Classic example: a carousel. Clicking "next" should slide left-to-right; clicking "previous" should slide the other way — even though both actions just update the same currentSlide number.

addTransitionType lets you attach a label to the cause of the update:

function nextSlide() {
  startTransition(() => {
    addTransitionType('next');
    setCurrentSlide((c) => c + 1);
  });
}

function previousSlide() {
  startTransition(() => {
    addTransitionType('previous');
    setCurrentSlide((c) => c - 1);
  });
}
Enter fullscreen mode Exit fullscreen mode

Then map each type to a different animation:

<ViewTransition
  enter={{ next: 'from-right', previous: 'from-left' }}
  exit={{ next: 'to-left', previous: 'to-right' }}
>
  <Page />
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

React also exposes the transition type as a browser view transition type, so you can scope the actual animation in plain CSS using :active-view-transition-type(...) instead of inline objects, if you'd rather keep the animation logic out of your components.

Where it gets genuinely interesting: Suspense

This is the part that made me actually want to try it. You can wrap a <Suspense> boundary in <ViewTransition> and animate the swap from fallback to real content:

<ViewTransition>
  <Suspense fallback={<Loading />}>
    <Component />
  </Suspense>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

You can even use it to coordinate image and font loading — wrapping an <img> and a @font-face declaration in <ViewTransition><Suspense> means React waits for both to be ready before revealing them together, instead of letting them flicker in independently as they finish.

But the default behavior has a rough edge. If you just wrap a Suspense boundary like above, every reveal animates — including ones where the content was already cached and should just appear instantly. That feels like the app got slower, not smoother.

The fix is to be explicit about what should and shouldn't animate:

<ViewTransition update="auto" default="none">
  <Suspense fallback={<Fallback />}>
    <Component />
  </Suspense>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

This gives you three rules worth keeping in mind whenever you combine <ViewTransition> with Suspense:

Fallbacks should appear immediately, without animation — the loading state shouldn't feel delayed
The swap from fallback to final content should animate — that's the actual payoff
Content that's already cached and doesn't suspend should appear immediately, without animation

Skip that last one and you'll animate things that were never actually loading, which just reads as unnecessary lag to the user.

Common mistakes to watch for

Mistake What happens Fix
Forgetting startTransition Nothing animates, state just updates instantly Wrap the update in startTransition
Animating every Suspense reveal Cached content feels artificially slow Use update="auto" default="none"
Using <ViewTransition> for React Native It's DOM-only for now Native/other-platform support isn't there yet
Expecting it to replace all animation libraries It only animates transition-triggered changes Keep a library for hover states, gestures, and non-transition micro-interactions

Should you adopt it now?

Honestly — probably, for the cases it's built for. It's stable, it requires no build step or extra dependency, and it's built on a real web standard rather than a JS-computed animation loop. If you're currently hand-rolling enter/exit animations with useEffect and CSS classes, this replaces that machinery directly.

Where I'd hold off: if you're deep into a library like Framer Motion for complex gesture-driven interactions (drag, spring physics, layout animations outside of React's transition model), there's no reason to rip that out.
<ViewTransition> solves a specific, narrower problem — animating mounts, unmounts, and content swaps tied to state transitions — not general-purpose animation.

A quick checklist

  • Is the state update wrapped in startTransition (or a Suspense reveal / useDeferredValue)?
  • Does this update need different animations depending on cause? → use addTransitionType
  • Are you animating a Suspense boundary? → set update="auto" default="none" to avoid animating cached content
  • Do you actually need a full animation library for this interaction, or does <ViewTransition> cover it?

Worth discussing

Has anyone here actually shipped <ViewTransition> in production yet? I'm curious whether the Suspense-fallback behavior needed more tuning than the docs suggest, and whether you kept an existing animation library around alongside it or dropped it entirely.

Top comments (0)