React 19.3 View Transitions: A Practical Guide

React 19.3 made <ViewTransition> stable. Three patterns with code: enter/exit, direction-aware slides and Suspense loading states, plus the one rule that trips everyone up and reduced motion.

Ananya RaoAnanya RaoAuthor14 October 20264 min read 104 views
React 19.3 View Transitions: A Practical Guide
In this article▾
  1. What View Transitions do, in one paragraph
  2. The rule that matters most: only Transitions animate
  3. Pattern 1: animate something appearing and disappearing
  4. Pattern 2: direction-aware slides
  5. Pattern 3: loading states that do not flash
  6. Respect reduced motion
  7. Browser support and fallbacks
  8. Also new in 19.3: Fragment refs
  9. Should you adopt it now?

React 19.3, released on 9 September 2026, made the <ViewTransition> component stable. That means you can animate between UI states, such as a list filtering, a slide changing or content arriving after loading, using the browser's View Transition API, without a third-party animation library. This guide shows the three patterns you are most likely to need, the one rule that trips everyone up, and how to keep it accessible.

What View Transitions do, in one paragraph

The browser's View Transition API takes a snapshot of the page before a change and after it, then animates between the two. By default you get a cross-fade; with CSS you can slide, scale or morph elements. MDN describes the goal well: reducing users' cognitive load and "helping them stay in context". React 19.3 wraps this in a component, so you mark which parts of your tree should animate and React calls the API at the right moment.

The rule that matters most: only Transitions animate

According to the React 19.3 release post, a <ViewTransition> animates only when the update happens inside one of these:

  • a startTransition() call

  • a <Suspense> boundary revealing its content

  • a useDeferredValue update

A plain setState from a click handler is treated as urgent, so it updates instantly with no animation. That is deliberate: typing in an input should never wait for an animation. If your transition "does nothing", this is almost always why.

Pattern 1: animate something appearing and disappearing

Wrap the element and update state inside startTransition:

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

function Details() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => startTransition(() => setOpen(o => !o))}>
        {open ? 'Hide' : 'Show'} details
      </button>
      {open && (
        <ViewTransition>
          <DetailsPanel />
        </ViewTransition>
      )}
    </>
  );
}

When <ViewTransition> is added to the tree, React runs an enter animation; when it is removed, an exit. There are also update (children change) and share (a named transition removed in one place and added in another, which gives you the "element morphs across the page" effect).

Pattern 2: direction-aware slides

A carousel should slide left when you go forward and right when you go back. React 19.3 handles this with addTransitionType, which tags the update with a reason:

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

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

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

<ViewTransition
  key={currentSlide}
  enter={{ next: 'from-right', previous: 'from-left' }}
  exit={{ next: 'to-left', previous: 'to-right' }}
>
  <Slide index={currentSlide} />
</ViewTransition>

The strings like from-right are class names you define in CSS using the view transition pseudo-elements. A trimmed version of the release post's CSS:

::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: 250ms;
}

::view-transition-new(.from-right) {
  --offset: 100%;
  animation-name: slide-in;
}

::view-transition-old(.to-left) {
  --offset: -100%;
  animation-name: slide-out;
}

@keyframes slide-in {
  from { transform: translateX(var(--offset)); opacity: 0; }
}

@keyframes slide-out {
  to { transform: translateX(var(--offset)); opacity: 0; }
}

Add the matching .from-left and .to-right rules with the offsets flipped. One note from me: the key on the wrapper is what makes React treat each slide as a new element, so you get enter and exit rather than update. Without it, you get a cross-fade of the changed content, which may be what you want anyway.

Pattern 3: loading states that do not flash

This is my favourite use. Wrap a <Suspense> boundary, and when the data arrives, React animates from the fallback to the content. The release post recommends this configuration:

<ViewTransition update="auto" default="none">
  <Suspense fallback={<Skeleton />}>
    <ProductDetails id={id} />
  </Suspense>
</ViewTransition>

Why those props? default="none" means the fallback appears immediately with no animation, so users get feedback straight away. update="auto" animates only the swap from skeleton to real content. And if the data is already cached, the content appears instantly, without a pointless fade.

Respect reduced motion

Some people get dizzy or nauseous from motion. Their operating system exposes a preference, and your CSS should honour it. This switches the animations off for them while the state changes still work:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Put this in your global stylesheet the day you add your first transition, not later.

Browser support and fallbacks

The release post says View Transitions are DOM-only for now, with React Native and other platforms to come. In browsers without the View Transition API, React degrades gracefully: the content still renders, just without animation. So you do not need feature detection in your components. Treat the animation as an enhancement, never as something your UI depends on to make sense.

Also new in 19.3: Fragment refs

Briefly, because it pairs well with transitions: <Fragment ref={...}> now gives you a FragmentInstance that can add event listeners, manage focus with focus() and focusLast(), and attach an IntersectionObserver or ResizeObserver through observeUsing(), all without adding a wrapper div that breaks your layout. The release post's InView example is worth copying into your toolbox.

Should you adopt it now?

The release post lists no major breaking changes in 19.3, so upgrading from 19.2 should be low-risk; read its notes on Strict Mode effects during hydration before you do. Then start small: one Suspense boundary that currently flashes, or one panel that pops in abruptly. Keep durations short, around 200 to 300 ms, so the interface feels responsive rather than theatrical. Good transitions are the ones users barely notice.

Ananya Rao

Written by

Ananya Rao

Keep reading

More from Ananya Rao

More from Ananya Rao

More in Web Development

Have a story of your own?

Publisha is free to start. Write with AI that keeps your voice, and publish in a click.