Skip to Content
React in a Week
Day 6 · Modern React 19

View transitions

Animate elements as they enter, leave and change with ViewTransition, custom View Transition Classes and transition types.

When a filter changes, cards vanish and others jump into their place in a single frame. It works, but the eye loses track of what moved. A short animation helps people see what changed.

Animating elements that are being added or removed used to be hard in React, because a removed element is gone from the DOM before you can animate it. Modern browsers have a View Transition API that solves this. The browser takes a picture of the page before a change and another after it, then animates between the two. React 19.3 makes this API available as a component, <ViewTransition>, so you don't have to coordinate the timing yourself.

Browser support

<ViewTransition> is stable as of React 19.3 and is imported from react. The sandboxes on this page pin React 19.3. In a browser without the View Transition API, your app works the same, but changes happen instantly without an animation. If an example below doesn't move, try it in a recent version of Chrome, Edge, Safari or Firefox.

Your first view transition

Wrap the part you want to animate in <ViewTransition>, then make the change inside startTransition. Click both buttons and compare.

Live code· Enter and exit

The first button cross-fades the card in and out. The second one makes it pop in and out with no animation. The component is the same; the only difference is how the state was updated.

That's the most important rule of this lesson. <ViewTransition> only animates updates that happen inside a Transition: a startTransition call, a form Action, a useDeferredValue update, or Suspense revealing content. A normal setState is urgent, and React shows urgent changes right away, without waiting for an animation.

React decides what kind of animation to run by looking at what happened to the <ViewTransition>:

TriggerWhen it happens
enterThe <ViewTransition> was added to the page
exitThe <ViewTransition> was removed from the page
updateSomething inside it changed, or it moved or resized
shareA named <ViewTransition> disappeared in one place and appeared in another

For each one, the default animation is a cross-fade.

Animating a list

To animate each item of a list separately, wrap each item in its own <ViewTransition> and move the key from the item onto the <ViewTransition>. The key tells React which item is which, so it knows which ones entered, which ones left and which ones only moved.

Live code· An animated filter

Cards that leave the filter fade out, new ones fade in, and the cards that stay slide to their new positions. You didn't write any code for the sliding: the browser sees that the same <ViewTransition> is in a different place and animates the move.

Quick check

You wrap each list item in <ViewTransition>, but leave key={book.id} on the <li> inside it. What goes wrong?

Custom animations with View Transition Classes

A cross-fade is a sensible default. To design your own animation, give the trigger a class name, and then style it in CSS with the browser's view transition pseudo-elements:

<ViewTransition enter="slide-in" exit="slide-out">

When this <ViewTransition> enters, React gives it the View Transition Class slide-in. In CSS, ::view-transition-new(.slide-in) is the "after" picture of an entering element, and ::view-transition-old(.slide-out) is the "before" picture of a leaving one.

Live code· View Transition Classes

New books slide in from the left, and removed books slide out to the right. In the prop you write the class name without a dot (enter="slide-in"). In the CSS selector you write it with one: (.slide-in).

Each trigger prop accepts:

  • a class name, as above;
  • "auto" for the browser's default cross-fade;
  • "none" to turn that animation off.

There's also a default prop for every trigger you don't set. For example, default="none" enter="auto" animates only entering, and nothing else.

Respect reduced motion

Some people get dizzy or distracted from movement on screen and ask their operating system to reduce it. Always add a prefers-reduced-motion media query, like the one at the bottom of the CSS above, that turns your animations off or makes them gentler.

Different animations with addTransitionType

Sometimes the same state change should animate differently depending on why it happened. A carousel's Next and Previous buttons both change the slide number, but Next should slide to the left and Previous to the right. addTransitionType labels a Transition so <ViewTransition> can choose:

startTransition(() => {
  addTransitionType('next');
  setIndex(index + 1);
});

Then pass an object to the trigger prop, mapping each type to a class:

Live code· Transition types

Two details are worth noticing:

  • key={index} on the <ViewTransition> makes React treat each slide as a new element. The old slide exits and the new one enters, so you can animate them in opposite directions.
  • default: 'auto' inside the object handles Transitions with no type, for example the first render, by using the normal cross-fade.

Routers use the same idea for back and forward navigation, so pages slide the right way.

Deep diveShared elements and Suspense reveals

Two more patterns are worth knowing about. You'll see them in real apps, although you don't need them for the Reading List today.

Shared element transitions. Give a <ViewTransition> a name. If a <ViewTransition> with that name disappears in one place and another with the same name appears somewhere else in the same Transition, the browser morphs one into the other. That's how a small book cover in a list can grow into the large cover on the detail page:

// In the list
<ViewTransition name={`cover-${book.id}`}>
  <img src={book.cover} className="thumbnail" alt="" />
</ViewTransition>

// On the detail page
<ViewTransition name={`cover-${book.id}`}>
  <img src={book.cover} className="large-cover" alt={book.title} />
</ViewTransition>

Each name must be unique on the page at any moment.

Animating a Suspense reveal. Wrap a <Suspense> boundary in a <ViewTransition>, and the switch from fallback to content cross-fades. React's docs recommend this combination, so that the fallback appears instantly and only the reveal animates:

<ViewTransition update="auto" default="none">
  <Suspense fallback={<DetailsSkeleton />}>
    <BookExtras id={id} />
  </Suspense>
</ViewTransition>
Try it

Animate deleting a book

In this list, the Delete button removes a book instantly. Make deleted books shrink and fade out over 300 ms, using a View Transition Class called shrink-out. Remember to add a reduced-motion rule.

Live code· Challenge: animate delete
Show solution
import { ViewTransition, startTransition, useState } from 'react';

const initialBooks = [
  { id: '1', title: 'The Hobbit' },
  { id: '2', title: 'Dune' },
  { id: '3', title: 'Circe' },
  { id: '4', title: 'Piranesi' },
];

export default function App() {
  const [books, setBooks] = useState(initialBooks);

  function handleDelete(id) {
    startTransition(() => {
      setBooks(books.filter((book) => book.id !== id));
    });
  }

  return (
    <ul className="list">
      {books.map((book) => (
        <ViewTransition key={book.id} exit="shrink-out">
          <li>
            {book.title}
            <button onClick={() => handleDelete(book.id)}>Delete</button>
          </li>
        </ViewTransition>
      ))}
    </ul>
  );
}
.list { list-style: none; padding: 0; display: grid; gap: 6px; max-width: 320px; }
.list li { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 8px; }

::view-transition-old(.shrink-out) {
  animation: 300ms ease-in both shrink-out;
}

@keyframes shrink-out {
  to { opacity: 0; transform: scale(0.8); }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(.shrink-out) { animation: none; }
}

You needed two changes. Wrapping setBooks in startTransition lets the <ViewTransition> animate at all. Adding exit="shrink-out" and the matching CSS gives the leaving item its custom animation. The books below still slide up to fill the gap, because their position changed inside a Transition.

Quick check

Which of these updates will make a <ViewTransition> animate?

Day 6 progress

0/7

On this page