01 article

React 19.3 Ships View Transitions, But Only for Lazy Updates

React 19.3 Ships View Transitions, But Only for Lazy Updates

React 19.3 Ships View Transitions, But Only for Lazy Updates

React 19.3 landed on npm on September 9, 2026, and the headline item is <ViewTransition> leaving experimental status. If you have been watching the browser's View Transitions API and wondering when the React integration would stop being a Canary-only science project, this is it. The awkward part was never the browser API itself. document.startViewTransition() is simple enough. The awkward part was orchestrating it around React's rendering: snapshotting the old tree, timing the DOM swap, and hoping nothing re-rendered mid-animation. React 19.3 takes that whole job off your hands.

I have shipped the raw API twice, and both times I ended up with a thin wrapper that did nothing but sequence calls correctly. That wrapper is now the framework's problem, which I am glad about.

The gate nobody warns you about

Here is the detail that trips people up on day one: the boundary only activates for updates React considers non-urgent. A plain setState call runs immediately and skips the animation entirely. The boundary fires for three kinds of work:

  • Updates wrapped in startTransition
  • Suspense boundaries revealing content
  • Updates coming from useDeferredValue

The minimal pattern looks like this:

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

function Switcher() {
  const [index, setIndex] = useState(0);

  return (
    <>
      <button
        onClick={() =>
          startTransition(() => setIndex(index + 1))
        }
      >
        Next
      </button>
      <ViewTransition enter="auto" exit="auto" default="none">
        <Video video={videos[index]} />
      </ViewTransition>
    </>
  );
}

Miss the startTransition wrapper and nothing animates. No error, no warning, just a static swap. I would argue this is the right design even though it will burn a generation of Stack Overflow questions. Typing into an input should never cross-fade. Navigation, deferred filtering, and lazy-loaded reveals are where motion earns its keep, and those are exactly the updates React already treats as interruptible.

There is a second behavior worth knowing. If a transition is running from state A to B and updates toward C and D arrive mid-animation, React batches them into the next transition, which then runs from B to D. The UI never falls behind, and you do not get a queue of stale animations playing catch-up.

Enter, exit, update, share

React classifies every activated boundary into one of four animation categories, and this is where the release gets genuinely useful:

CategoryWhen it fires
enterA ViewTransition is first inserted during the Transition
exitA ViewTransition is removed during the Transition
updateA boundary stays mounted and its contents change
shareA named boundary disappears in one subtree while the same name appears in another

That last one is React's version of a shared-element transition. List item morphing into a detail view, thumbnail growing into a hero image, the effect that normally takes a router, a portal, and some position math. With React you give both boundaries the same name and the framework does the rest.

Which brings the naming rule: do not name anything unless it is a shared element. React auto-generates unique names for ordinary boundaries. An explicit name must be unique across the whole application, so a view-transition-name="card" copy-pasted into three components is a bug you will find the hard way.

Two footguns to read before you ship

The boundary animates a captured image of the region, not live elements. That is what makes the animation cheap and continuous, but it also means a spinner inside a transitioning region will appear frozen during the cross-fade. Components with independently moving parts need their own nested <ViewTransition> boundaries or they look broken mid-flight.

Second, React does not automatically respect prefers-reduced-motion. The browser does not do it for you either, so the accessible default is opt-in work on your side. Something like this belongs in your global stylesheet before you ship a single transition:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 0s;
  }
}

A zero-duration animation still swaps states instantly, so reduced-motion users get the update without the travel. Writing this late is how you end up with a site that quietly gives people vertigo.

Telling direction from state

The release also stabilizes addTransitionType, which labels why a transition happened. A carousel can tag the same state update as next or previous and pick a slide-left or slide-right animation to match. For anything more custom, the onEnter, onExit, onShare, and onUpdate callbacks hand you the old and new pseudo-elements plus the transition-type array, and you can drive them through the Web Animations API. For most apps the class props plus a bit of CSS cover it. The callbacks are escape hatches, not the main road.

The rest of the release

Three smaller things shipped alongside. Fragment Refs give a component a limited set of DOM methods over a group of rendered elements, which papers over the "I need the actual nodes but I return a fragment" problem. use(browser()) opts a component out of server rendering entirely: the nearest Suspense fallback stays in the HTML and rendering continues client-side, with no hydration mismatch and no effect that discovers it lives in a browser after the fact. React DOM also gained Trusted Types support, and direct <Context> provider syntax now works in Server Components.

One platform caveat: <ViewTransition> is DOM-only for now. React says React Native support is in progress, so shared component libraries should not assume the boundary is renderer-independent.

Should you adopt it

If you are already on React 19, the upgrade is a refinement, not a migration. The sensible path is narrow on purpose: pick one or two updates that represent a real change of view, wrap them in startTransition, wrap the affected region in a boundary, and write the reduced-motion CSS first. Do not call document.startViewTransition() by hand anywhere React owns the DOM. The docs explicitly advise against it, and mixing the two orchestration paths is asking for dropped frames.

What keeps coming back to me is how much glue code this deletes. The thumbnail-to-hero morph used to be a library conversation. Now it is a prop and a name. I am skeptical of most animation churn in this industry, but transitions that the browser compositor runs for free, around updates React already considers interruptible, is the right shape. The urgency gate is the part most people will complain about in week one and quietly thank in month one.

Share

Comments