cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

View transitions

Animate between screens with the browser's View Transition API and React's ViewTransition: what triggers it, custom classes, types, and shared elements.

By the end, you can
  • 01
    Animate a page change with the browserCall document.startViewTransition, read its promises, style its pseudo-elements, and fall back when a browser doesn't have it.
  • 02
    Animate React updates with ViewTransitionWrap part of the page in <ViewTransition>, and know which updates animate it: transitions, Suspense reveals, and deferred values.
  • 03
    Choose the animationGive enter, exit, and update animations your own CSS classes, pick them by cause with addTransitionType, move a shared element with name, and respect reduced motion.

A page that changes in a single frame

When you click a link or a tab on most pages, the old content is gone and the new content is there, from one frame to the next. For a small change, like a number, that's right. When a whole panel or page swaps, it's easy to lose your place: what moved, and where did it go? A short animation between the two, like a fade or a slide, shows the change.

Browsers have a built-in way to do this, the View Transition API. Here is a shop page with two buttons that do the same thing: switch between the teas and your cart. The result is live: click each button a few times and compare.

The same switch, plain and with a view transitionPop out in the code editor (opens in a new tab)JavaScript
const pages = {  teas: "<h3>Teas</h3><p>Green tea, chai, mint tea.</p>",  cart: "<h3>Your cart</h3><p>2 items: chai and mint tea. Total: $7</p>",};let current = "teas"; function switchPage() {  current = current === "teas" ? "cart" : "teas";  document.querySelector("#page").innerHTML = pages[current];} document.querySelector("#plain").addEventListener("click", switchPage); document.querySelector("#animated").addEventListener("click", () => {  if (!document.startViewTransition) return switchPage();  document.startViewTransition(switchPage);});
ResultLoading…

Lines 1 to 4 hold the two pages as HTML text. switchPage on lines 7 to 10 flips current and puts the other page into #page with innerHTML (the Modifying the document lesson covers it). Line 12 runs it straight from the Switch button: the page changes at once.

Line 16 is the new part. Switch with a view transition passes the same switchPage to document.startViewTransition, and the old page fades into the new one. Line 15 checks that the browser has the API first, and if it doesn't, just switches. The styles.css tab makes the fade last 0.8 seconds instead of the browser's 0.25, so it's easy to see. Its last rule turns the animation off for people who ask their system for less motion; more on that below.

Three steps of a view transition. Step 1: the browser takes a picture of the old page, showing Teas. Step 2: your update function changes the real page to Your cart. Step 3: the browser shows the old picture fading out over the new page fading in, then removes the pictures.1. picture of old pageoldTeas2. your update runspageYour cartswitchPage()3. browser animatesscreenTeasYour cartold fades out,new fades inThen the pictures are removed and the real page shows.
document.startViewTransition(switchPage) takes a picture of the page, runs switchPage, and fades from the old picture to the new page. Your update only changes the page; the animation is the browser's.

Since React 19.3, React has a stable component for this, <ViewTransition>. Here is the same switch in React. Click both buttons again:

The same switch in ReactPop out in the code editor (opens in a new tab)JSX
import { ViewTransition, startTransition, useState } from "react"; const pages = {  teas: { title: "Teas", text: "Green tea, chai, mint tea." },  cart: { title: "Your cart", text: "2 items: chai and mint tea. Total: $7" },}; export default function App() {  const [current, setCurrent] = useState("teas");  const next = current === "teas" ? "cart" : "teas";  const page = pages[current];   return (    <>      <button onClick={() => setCurrent(next)}>Switch</button>      <button onClick={() => startTransition(() => setCurrent(next))}>        Switch with a view transition      </button>      <ViewTransition>        <section className="page">          <h3>{page.title}</h3>          <p>{page.text}</p>        </section>      </ViewTransition>    </>  );}
ResultLoading…

Lines 19 to 24 wrap the page in <ViewTransition>, imported from react on line 1. Line 15's Switch sets the state directly, and the page changes at once. Line 16's button sets the same state inside startTransition, and the page fades. You never call startViewTransition yourself: React calls it when an update inside a startTransition changes something in a <ViewTransition>.

Real-life analogyA photo before the furniture moves

A shop changes its window display at night. Before the decorator moves anything, she hangs a big photo of the old window in front of the glass. She rearranges everything behind it. Then the photo slowly fades, and people outside see the old window melt into the new one. They never see the boxes and ladders in between.

In real life: A decorator takes a photo of the room before moving anything
In React: The browser takes a picture of the page before your update
In real life: The furniture is moved while the photo is up
In React: Your update changes the real page
In real life: Visitors see the photo melt into the new room
In React: The old picture fades out over the new page
In real life: The photo is taken down when the new room shows
In React: The pictures are removed when the animation ends
The main idea

A view transition animates the page from how it looked to how it looks after an update. The browser takes a picture of the page, runs your update, and animates from the old picture to the new page. In React, wrap part of the page in <ViewTransition>, and updates in a transition animate it.

The browser's View Transition API

React's component is built on the browser's API, so it pays to see that first. document.startViewTransition takes one function, the update callback, which changes the page. It returns an object, a ViewTransition, with three promises for its steps. This card logs each step. Click Show details and read the console under the result:

One view transition, step by stepPop out in the code editor (opens in a new tab)JavaScript
const card = document.querySelector("#card");const button = document.querySelector("#toggle"); function update() {  console.log("2. update runs. The old page is already a picture.");  card.classList.toggle("open");  button.textContent = card.classList.contains("open") ? "Hide details" : "Show details";} button.addEventListener("click", async () => {  if (!document.startViewTransition) return update();  const transition = document.startViewTransition(update);  console.log("1. startViewTransition returned.");  await transition.updateCallbackDone;  console.log("3. updateCallbackDone: the page has changed.");  await transition.ready;  const animations = document.documentElement.getAnimations({ subtree: true });  const parts = new Set(animations.map((animation) => animation.effect.pseudoElement));  console.log("4. ready. Animating:", [...parts].join(", "));  await transition.finished;  console.log("5. finished. The pictures are gone.");});
ResultLoading…

update on lines 4 to 8 opens or closes the card and changes the button's text. Line 11 is the check from the overview: without the API, it just runs update. Line 12 starts the view transition. Then the code waits for each promise with await. The console shows five lines, in this order:

  1. 1. startViewTransition returned. Line 13 runs right after the call. The update hasn't run yet: the browser first takes a picture of the page as it is.
  2. 2. update runs. A moment later the browser calls update (line 5), and the real page changes behind the picture.
  3. 3. updateCallbackDone (line 15). The promise on line 14 resolves when the update has finished.
  4. 4. ready. Animating: … (line 19). The ready promise on line 16 resolves when the animation is about to start. Lines 17 and 18 list every animation on the page, and their names, like ::view-transition-old(tea-card), are the parts the next section explains.
  5. 5. finished. (line 21) The finished promise on line 20 resolves when the animation has ended and the pictures are gone.

The update must change the page before it returns (or before the promise it returns resolves), because the browser takes the new picture right after it. The object also has skipTransition(), which jumps to the end with no animation; the update still runs.

Browsers added the API at different times. In the table, the middle column is the version that first had document.startViewTransition(update). The last column is the version that first took an object, like this, which can carry types (you'll meet them later):

The object form, with typesJavaScript
document.startViewTransition({
  update: switchPage,
  types: ["forward"],
});

How to read the table: each row is a browser, and each cell is the first version with that form of the call.

Browser support for document.startViewTransition, from MDN's compatibility data, checked on 26 October 2026
BrowserstartViewTransition(update)startViewTransition({ update, types })
Chrome and EdgeVersion 111Version 125
Safari, on Mac and iPhoneVersion 18Version 18.2
FirefoxVersion 144Version 147

Every current version of the major browsers has both forms, and MDN marks view transitions in one page as Baseline, newly available since October 2025, when Firefox 144 shipped them. Older browsers are still out there, so keep the check on line 11: without the API the page simply changes with no animation, which is a fine fallback.

Pictures you can style with CSS

While a view transition runs, the browser puts its pictures in a tree of pseudo-elements on top of the page. A pseudo-element is a part of the page that CSS can style, but that isn't in your HTML, like ::before. The console line from step 4 named them. Each part of the page that animates on its own gets four:

  • ::view-transition-group(name) moves and resizes from the old position and size to the new ones.
  • ::view-transition-image-pair(name) holds the two pictures together.
  • ::view-transition-old(name) is the picture from before the update. By default it fades out.
  • ::view-transition-new(name) is the page after the update, live. By default it fades in.
The pseudo-element tree for the tea card. At the top, ::view-transition. Under it, two groups: group(root) for the rest of the page and group(tea-card). Under group(tea-card), image-pair(tea-card). Under that, two pictures: old(tea-card), the picture before the update, and new(tea-card), the page after it.::view-transitiongroup(root)group(tea-card)the rest of the page,with its own pair insideimage-pair(tea-card)old(tea-card)new(tea-card)
Every named part gets a group that moves and resizes, holding an image pair with the old picture and the new one, which cross-fade. The rest of the page is the part named root.

The name comes from the CSS property view-transition-name. The whole page is named root by default. In the step-by-step example, the styles.css tab gives the card its own name on line 2: view-transition-name: tea-card. That's why the card grows into its new size instead of fading with the rest of the page. Lines 14 to 16 of that tab style its group: the move and resize take one second. Press Edit & run, open styles.css, change line 2 to view-transition-name: none, and click again: the card no longer grows on its own, and only the whole page fades.

Two rules come with names. A name may be on the page only once at a time, or the browser skips the whole transition. And a name is how the browser pairs the old and new picture of the same thing, which is what makes an element seem to move.

The animation runs in about a quarter of a second, too fast to read. The playground runs a real view transition on this page and stops it at its first frame, so you can drag it from start to end and see what the browser draws:

Playground: stop a view transition halfway
What Show details runsJavaScript
const transition = document.startViewTransition(() => {
  flushSync(() => setOpen(!open));
});

await transition.ready;
const animations = document.documentElement
  .getAnimations({ subtree: true })
  .filter((animation) => animation.effect.pseudoElement.includes("(tea-card)"));
for (const animation of animations) animation.pause();

// The slider: every animation jumps to the same point.
animation.currentTime = duration * (progress / 100);
The pagedetails hidden

Assam black tea

Try it yourself

Press Show details. The browser takes a picture of the card, React changes it inside flushSync, and the animation pauses at its first frame.

This runs a real view transition on this page with the browser's own API: the code beside it, as it is. It pauses the animations as soon as they start, and the slider sets their time. The numbers are what the browser reports for the pictures. Scrolling away lets the transition finish. Reset puts the card back.

At 0%, the old picture's opacity is 1 and the new picture's is 0. As you drag, one fades out while the other fades in, and the group's width grows from the small card to the big one. At 100%, only the new picture shows; let it finish, and the real card is back. The code beside it uses flushSync from react-dom inside the update, because the update must change the page before it returns, and a React set call alone would only ask for a render. That's fine for a demo, but React has a better way, next.

Always respect reduced motion

Some people turn on a system setting that asks for less motion, because animation can make them feel dizzy or sick. Neither the browser nor React turns view transitions off for them. Every example in this lesson ends with this rule, which turns the animations off when that setting is on, so the page simply changes:

No view transition animations for reduced motionCSS
/* No animation for people who ask their system for less motion. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Line 2 is the media query that matches when the setting is on. Lines 3 to 5 select every view transition group and picture, with * for any name, and line 6 removes their animations.

React's ViewTransition component

In React you don't call startViewTransition or write view-transition-name. You wrap part of the page in <ViewTransition>, and React does both when that part changes in a transition. It became stable in React 19.3, released in September 2026, after a year as an experimental API; every example here runs React 19.3.1. Here are a tea's details that come and go. Click Show details and Hide details, and watch the console:

Details that fade in and outPop out in the code editor (opens in a new tab)JSX
import { ViewTransition, startTransition, useState } from "react";import "./watchTransitions.js"; function Details() {  return (    <ViewTransition>      <p className="card">Grown in Assam. Strong and malty. Best with milk.</p>    </ViewTransition>  );} export default function App() {  const [open, setOpen] = useState(false);   function toggle() {    startTransition(() => setOpen(!open));  }   return (    <>      <h3>Assam black tea</h3>      <button onClick={toggle}>{open ? "Hide details" : "Show details"}</button>      {open && <Details />}    </>  );}
ResultLoading…

Details on lines 4 to 10 returns its paragraph inside a <ViewTransition>. Line 16 flips open inside startTransition, and line 23 shows Details while open is true. The paragraph fades in and out, and the console prints View transition started. Types: none each time. That line comes from the watchTransitions.js tab, imported on line 2: it wraps document.startViewTransition to print a line whenever React calls it.

Only what's inside a <ViewTransition> animates. React turns off the browser's picture of the whole page, the part named root, so the heading and the button change at once and stay clickable while the paragraph fades.

Press Edit & run and change line 16 to setOpen(!open);, without startTransition. The paragraph now appears and goes at once, and the console stays quiet: React never called the API.

React picks one of four kinds of animation for each <ViewTransition>, by what happened to it in the transition. All four are a cross-fade unless you choose otherwise. How to read the table: the first column is the kind, the second when React uses it, and the third where you've seen it here.

Four ways a ViewTransition animates
KindWhen it happensIn this lesson
enterThe <ViewTransition> is added to the page in a transition.The details paragraph appears after Show details.
exitThe <ViewTransition> is removed from the page in a transition.The details paragraph goes after Hide details.
updateSomething React changes inside it, or its size or place, changes in a transition.The page switch in the overview, and the cart count.
shareA named one is removed and another with the same name is added in the same transition.The photo that flies from the list to the details page.

An Activity counts too. When an <Activity> switches between visible and hidden in a transition, a <ViewTransition> at the top of it enters as it shows and exits as it hides, and the hidden part keeps its state.

Enter and exit have one rule. React only animates a <ViewTransition> that comes before any DOM element in the part of the page that was added or removed. If Details wrapped it in a <div>, like this, nothing would animate in or out, because the <div> is the first thing added:

Details that never animate in (a div comes first)JSX
function Details() {
  return (
    <div>
      <ViewTransition>
        <p className="card">Grown in Assam. Strong and malty. Best with milk.</p>
      </ViewTransition>
    </div>
  );
}

Line 3 is the <div> that comes first. React keeps this rule so that a small change deep inside a page doesn't make large parts of it animate by surprise. Exercise 2 lets you try it.

Read it in React's own code

React's startViewTransition in ReactFiberConfigDOM.js calls ownerDocument.startViewTransition({ update, types }) with the object form, at lines 2186 to 2274, and cancelRootViewTransitionName is where it turns off the whole-page picture. If the call throws, React applies the update with no animation. The comment in its catch block explains why: React only supports the version of the API that takes an object. So in a browser with only the older form (the middle column of the support table), React's view transitions don't animate at all.

The catch block in ReactFiberConfigDOM.js, lines 2404 to 2410JavaScript
  } catch (x) {
    // We use the error as feature detection.
    // The only thing that should throw is if startViewTransition is missing
    // or if it doesn't accept the object form. Other errors are async.
    // I.e. it's before the View Transitions v2 spec. We only support View
    // Transitions v2 otherwise we fallback to not animating to ensure that
    // we're not animating with the wrong animation mapped.

Which updates animate

A plain set call is urgent: React shows it as soon as it can, and an animation would only get in the way. So React animates a <ViewTransition> only for updates that are allowed to take a moment. There are three:

  • Updates in a transition: inside startTransition, or the startTransition that useTransition gives you (the Transitions lesson covers both).
  • A Suspense reveal: a Suspense boundary swapping its fallback for its content.
  • The background render where a deferred value catches up.
Left: an urgent update, setCount(2) in a click, changes the page at once, with no animation. Right: three updates that React treats as transitions, startTransition, a Suspense reveal, and a deferred value, all lead to a ViewTransition window that animates.Urgent: shown at oncesetCount(2)page2 in the cartno animationAllowed to wait: animatedstartTransition(…)Suspense shows its contentuseDeferredValue catches upanimates<ViewTransition>React calls startViewTransition
React animates a <ViewTransition> only for updates that are allowed to wait: a transition, a Suspense boundary showing its content, or a useDeferredValue catching up. Urgent updates show at once, as before.

This example has all three kinds of update next to one <ViewTransition>. Try each one, and watch the console:

Three updates, one ViewTransitionPop out in the code editor (opens in a new tab)JSX
import { ViewTransition, startTransition, useDeferredValue, useState } from "react";import "./watchTransitions.js"; export default function App() {  const [count, setCount] = useState(1);  const [search, setSearch] = useState("");  const deferredSearch = useDeferredValue(search);   return (    <>      <button onClick={() => setCount(count + 1)}>Add one now</button>      <button onClick={() => startTransition(() => setCount(count + 1))}>        Add one in a transition      </button>      <input placeholder="Search teas" value={search} onChange={(event) => setSearch(event.target.value)} />      <ViewTransition>        <p className="card">          {count} in the cart. Searching for: {deferredSearch || "nothing"}        </p>      </ViewTransition>    </>  );}
ResultLoading…
  • Add one now (line 11) sets the count directly. The number changes at once, and the console stays quiet.
  • Add one in a transition (line 12) sets it inside startTransition. The paragraph cross-fades, and the console prints View transition started. Types: none.
  • Type in Search teas. Line 15 sets search urgently, so the box shows your key at once. Line 7 gives a deferred copy, and line 18 shows that copy inside the <ViewTransition>. When it catches up, a moment later, the paragraph animates and the console prints a line.

A Suspense reveal animates too. Here the reviews come from a pretend server in the data.js tab, which answers after 1.5 seconds. Press Run again on the result to watch it again:

A Suspense fallback that fades into the contentPop out in the code editor (opens in a new tab)JSX
import { Suspense, ViewTransition, use } from "react";import { fetchReviews } from "./data.js";import "./watchTransitions.js"; function Reviews() {  const reviews = use(fetchReviews("assam"));  return (    <ul className="card">      {reviews.map((review) => <li key={review}>{review}</li>)}    </ul>  );} export default function App() {  return (    <>      <h3>Assam black tea</h3>      <ViewTransition>        <Suspense fallback={<p className="card">Loading reviews…</p>}>          <Reviews />        </Suspense>      </ViewTransition>    </>  );}
ResultLoading…

Line 6 reads the reviews with use, which suspends until they arrive. Lines 18 to 22 put the Suspense boundary inside a <ViewTransition>. First you see Loading reviews…. When the reviews arrive, React swaps the fallback for the list inside a view transition, so it fades instead of jumping, and the console prints a line. Because the <ViewTransition> wraps the whole boundary, React treats the swap as an update. A <ViewTransition> around the fallback and another around the content would make it an exit and an enter instead.

How to read the table: each row is an update next to a <ViewTransition>, whether React animates it, and why.

Which updates animate
The updateAnimates?Why
setCount(count + 1) in a clickNo.It's urgent: React shows it at once, as it always has.
startTransition(() => setCount(count + 1))Yes.A transition may take a moment, so React has time to take the pictures and animate.
A <Suspense> boundary showing its contentYes.React treats the reveal like a transition and animates from the fallback to the content.
The render where useDeferredValue catches upYes.That second render is a background render, like a transition.
Typing in a box: setSearch(event.target.value)No.Typing must show at once. Only the deferred render after it animates.

Your own animations: classes and types

A cross-fade suits most changes, but a carousel should slide: left when you go forward, right when you go back. React lets you name a CSS class for each kind of animation, with the props enter, exit, update, share, and default. React calls these View Transition Classes. And because both buttons make the same kind of change, you also tell React why the change happened, with addTransitionType, which became stable in React 19.3 together with <ViewTransition>. Press Next and Previous:

A tea of the day carousel that slides the right wayPop out in the code editor (opens in a new tab)JSX
import { ViewTransition, addTransitionType, startTransition, useState } from "react";import "./watchTransitions.js"; const teas = ["Green tea", "Chai", "Mint tea", "Earl Grey"]; export default function App() {  const [index, setIndex] = useState(0);   function go(step, type) {    startTransition(() => {      addTransitionType(type);      setIndex((index + step + teas.length) % teas.length);    });  }   return (    <>      <button onClick={() => go(-1, "previous")}>Previous</button>      <button onClick={() => go(1, "next")}>Next</button>      <ViewTransition        key={index}        enter={{ next: "from-right", previous: "from-left" }}        exit={{ next: "to-left", previous: "to-right" }}      >        <p className="slide">Tea of the day: {teas[index]}</p>      </ViewTransition>    </>  );}
ResultLoading…

go on lines 9 to 14 starts a transition. Line 11 calls addTransitionType(type) inside it, which labels the transition "next" or "previous"; the buttons on lines 18 and 19 pass the label. Line 21 gives the <ViewTransition> a key that changes with every slide, so React removes the old slide and adds a new one: an exit and an enter.

Lines 22 and 23 pick the classes. An object maps a type to a class: for a "next" transition the new slide gets from-right and the old one to-left, and for "previous" it's the other way round. The console prints the type each time, like View transition started. Types: previous.

The styles.css tab draws the slides. Lines 8 to 11 select the pictures by class, with a dot: ::view-transition-new(.from-right) is the new picture of any part that has the class from-right. Lines 13 to 16 are the keyframes: the old slide moves 60 pixels away and fades out, and the new one comes from the other side. Lines 19 to 25 are the reduced motion rule.

A few more things to know about classes:

  • A plain string, like enter="slide-up", is used for every transition. "auto" is the browser's cross-fade, and "none" turns that kind off.
  • In an object, a default key is used when no type matches. Without one, a type you didn't list gets the cross-fade.
  • default="none" turns off every kind you don't name, so <ViewTransition default="none" enter="auto"> animates only when it's added.
  • React also passes the types to the browser, so CSS can read them with :active-view-transition-type(next), and it has event props, like onEnter, for animating from JavaScript with element.animate().

One element moving between two screens

Shops often show a small photo in a list, and the same photo big on the details page. When you open the details, the photo can fly from its place in the list and grow into the big one. This is a shared element transition. Give the <ViewTransition> on both screens the same name. Click See details, then Back:

A photo that grows into the details pagePop out in the code editor (opens in a new tab)JSX
import { ViewTransition, startTransition, useState } from "react";import "./watchTransitions.js"; function Thumbnail({ onOpen }) {  return (    <div className="thumb">      <ViewTransition name="assam-photo">        <div className="photo small">Assam</div>      </ViewTransition>      <button onClick={onOpen}>See details</button>    </div>  );} function Details({ onClose }) {  return (    <article>      <ViewTransition name="assam-photo">        <div className="photo big">Assam</div>      </ViewTransition>      <p>Strong and malty. Best with milk.</p>      <button onClick={onClose}>Back</button>    </article>  );} export default function App() {  const [open, setOpen] = useState(false);  const show = (value) => startTransition(() => setOpen(value));   return open ? <Details onClose={() => show(false)} /> : <Thumbnail onOpen={() => show(true)} />;}
ResultLoading…

Thumbnail (lines 4 to 13) shows the small photo, and Details (lines 15 to 25) the big one. Lines 7 and 18 give both photos the name assam-photo. Line 31 shows one screen or the other, and line 29 switches them in a transition. In that transition, one named <ViewTransition> is removed and another with the same name is added, so React pairs them for a share animation: the photo's group moves and resizes from the small box to the big one. The text and the buttons aren't in a <ViewTransition>, so they change at once.

Left, a list page window with a small photo card named assam-photo and a See details button. Right, a details page window with a big photo card, also named assam-photo. An arrow from the small photo to the big one is labelled share: it moves and grows.listAssamassam-photoSee detailsshare:it moves and growsdetailsAssamassam-photoone name on each side, never two at once on the same side
The list's photo leaves and the details page's photo arrives in the same transition, and both are in a <ViewTransition name="assam-photo">. React pairs them, so the photo moves and grows instead of fading out and in.

Unlike enter and exit, a shared pair doesn't have to be at the top of what changed: line 18's is inside an <article>. Two rules apply:

  • Use a name only for shared elements. Without one, React makes a unique name for each <ViewTransition>, which is what you want everywhere else.
  • Each name once at a time. In a list, put the item's id in the name, like photo-assam. Two with the same name on the page make React log an error in development, and the browser can't run the transition.

Also, if either photo is outside the visible part of the page, React doesn't pair them, so a photo never flies in from far off screen.

A tea shop with animated pages

Here is everything together: a list of three teas and a details page. Each photo flies to the details page and back, the transitions carry a type for the direction, and adding to the cart stays instant. Click a photo, Add to cart, and Back:

Three teas with shared photosPop out in the code editor (opens in a new tab)JSX
import { ViewTransition, addTransitionType, startTransition, useState } from "react";import "./watchTransitions.js"; const teas = [  { id: "assam", name: "Assam", color: "", note: "Strong and malty." },  { id: "sencha", name: "Sencha", color: "green", note: "Fresh and grassy." },  { id: "mint", name: "Mint", color: "mint", note: "Cool and sweet." },]; function Photo({ tea, size }) {  return (    <ViewTransition name={`photo-${tea.id}`}>      <div className={`photo ${size} ${tea.color}`}>{tea.name}</div>    </ViewTransition>  );} export default function App() {  const [openId, setOpenId] = useState(null);  const [cart, setCart] = useState(0);  const open = teas.find((tea) => tea.id === openId);   function go(id, type) {    startTransition(() => {      addTransitionType(type);      setOpenId(id);    });  }   return (    <>      <p>Cart: {cart}</p>      {open ? (        <article>          <Photo tea={open} size="big" />          <p>{open.note}</p>          <button onClick={() => setCart(cart + 1)}>Add to cart</button>          <button onClick={() => go(null, "back")}>Back</button>        </article>      ) : (        <div className="grid">          {teas.map((tea) => (            <button key={tea.id} aria-label={tea.name} onClick={() => go(tea.id, "forward")}>              <Photo tea={tea} size="small" />            </button>          ))}        </div>      )}    </>  );}
ResultLoading…

Photo on lines 10 to 16 wraps every photo in a <ViewTransition> named after its tea, on line 12: photo-assam, photo-sencha, photo-mint. The list on lines 41 to 47 uses it small, and the details page on line 35 big. go (lines 23 to 28) opens or closes a tea in a transition, labelled "forward" or "back" on lines 43 and 38, and the console prints the type each time.

Line 37's Add to cart is urgent on purpose: the count on line 32 changes at once, and the console stays quiet. A cart count should never wait for an animation. The types aren't used by any class yet; in exercise 5 you use them the way the carousel does.

Animated or at once?
  • onClick={() => setOpen(true)}
  • onClick={() => startTransition(() => setOpen(true))}
  • const [isPending, startTransition] = useTransition(); … startTransition(() => setTab("cart"))
  • const deferredQuery = useDeferredValue(query);
  • A <Suspense> boundary finishes loading and shows its content
  • startTransition(() => { addTransitionType("next"); setIndex(index + 1); })
  • A <div> with a <ViewTransition> inside it is added in a transition
  • onChange={(event) => setQuery(event.target.value)}
Try it yourself
0 of 8 correct

Each card is an update to a page with a ViewTransition in it. Sort it: does React animate it, or does the change show at once?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "A ViewTransition animates every change inside it." No. It animates only updates in a transition, a Suspense reveal, or a deferred value catching up. A plain set call shows at once.
  • "I should call document.startViewTransition in my React code." No. React calls it for you, and a transition started by someone else on the page is interrupted by React's. Use <ViewTransition> and startTransition.
  • "The browser animates my real elements." No. It animates pictures of them, in pseudo-elements on top of the page. The real page changes at once, underneath.
  • "Every ViewTransition needs a name." No. React makes unique names itself. Give a name only to pair two of them for a shared element, and keep each name unique on the page.
  • "People who prefer less motion won't see the animation." Only if you add the prefers-reduced-motion rule. Neither React nor the browser does it for you.
  • "Old browsers will break." No. Where the API is missing, React applies the update with no animation, and the page works as before.

Practice exercises

Each exercise shows its result under the code, or when you press Edit & run. Watch the console: a View transition started line means React animated. Try a guess first, then use the hints.

Exercise 1 · Warm-upPredict an urgent update

Does the paragraph animate when you click Add one? Answer first, then press Edit & run, click, and watch the console.

import { ViewTransition, useState } from "react";
import "./watchTransitions.js";

export default function App() {
  const [count, setCount] = useState(1);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Add one</button>
      <ViewTransition>
        <p className="card">{count} in the cart</p>
      </ViewTransition>
    </>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upMake the details animate in

    Show details uses startTransition, but the details never fade in and the console stays quiet. Fix Details. What does the console print when you press Show details?

    import { ViewTransition, startTransition, useState } from "react";import "./watchTransitions.js"; function Details() {  return (    <div>      <ViewTransition>        <p className="card">Grown in Assam. Strong and malty. Best with milk.</p>      </ViewTransition>    </div>  );} export default function App() {  const [open, setOpen] = useState(false);   function toggle() {    startTransition(() => setOpen(!open));  }   return (    <>      <h3>Assam black tea</h3>      <button onClick={toggle}>{open ? "Hide details" : "Show details"}</button>      {open && <Details />}    </>  );}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAnimate a sale price by hand

      This plain JavaScript page switches a sale price at once. Make the switch fade with the browser's API, with a fallback. What does the price say after one click?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function switchSale() {  const price = document.querySelector("#price");  price.textContent = price.textContent === "$4" ? "Sale: $3" : "$4";} document.querySelector("#sale").addEventListener("click", switchSale);
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRespect reduced motion

        The details fade slowly, and nothing turns that off for people who ask for less motion. Add a rule to styles.css that does. Which media feature does your rule test?

        import { ViewTransition, startTransition, useState } from "react";import "./watchTransitions.js"; function Details() {  return (    <ViewTransition>      <p className="card">Grown in Assam. Strong and malty. Best with milk.</p>    </ViewTransition>  );} export default function App() {  const [open, setOpen] = useState(false);   function toggle() {    startTransition(() => setOpen(!open));  }   return (    <>      <h3>Assam black tea</h3>      <button onClick={toggle}>{open ? "Hide details" : "Show details"}</button>      {open && <Details />}    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeSlide the right way

          Both buttons slide the new tea in from the right, even Previous. Use transition types so Previous slides the other way. What does the console print when you press Previous?

          import { ViewTransition, startTransition, useState } from "react";import "./watchTransitions.js"; const teas = ["Green tea", "Chai", "Mint tea", "Earl Grey"]; export default function App() {  const [index, setIndex] = useState(0);   function go(step) {    startTransition(() => {      setIndex((index + step + teas.length) % teas.length);    });  }   return (    <>      <button onClick={() => go(-1)}>Previous</button>      <button onClick={() => go(1)}>Next</button>      <ViewTransition key={index} enter="from-right" exit="to-left">        <p className="slide">Tea of the day: {teas[index]}</p>      </ViewTransition>    </>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFix the photo names

            The photos don't fly to the details page, and the console shows errors. Fix the name on line 12. What name does Sencha's photo get?

            import { ViewTransition, addTransitionType, startTransition, useState } from "react";import "./watchTransitions.js"; const teas = [  { id: "assam", name: "Assam", color: "", note: "Strong and malty." },  { id: "sencha", name: "Sencha", color: "green", note: "Fresh and grassy." },  { id: "mint", name: "Mint", color: "mint", note: "Cool and sweet." },]; function Photo({ tea, size }) {  return (    <ViewTransition name="photo">      <div className={`photo ${size} ${tea.color}`}>{tea.name}</div>    </ViewTransition>  );} export default function App() {  const [openId, setOpenId] = useState(null);  const [cart, setCart] = useState(0);  const open = teas.find((tea) => tea.id === openId);   function go(id, type) {    startTransition(() => {      addTransitionType(type);      setOpenId(id);    });  }   return (    <>      <p>Cart: {cart}</p>      {open ? (        <article>          <Photo tea={open} size="big" />          <p>{open.note}</p>          <button onClick={() => setCart(cart + 1)}>Add to cart</button>          <button onClick={() => go(null, "back")}>Back</button>        </article>      ) : (        <div className="grid">          {teas.map((tea) => (            <button key={tea.id} aria-label={tea.name} onClick={() => go(tea.id, "forward")}>              <Photo tea={tea} size="small" />            </button>          ))}        </div>      )}    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each React question, ask two things: is the update in a transition, a Suspense reveal, or a deferred value? And where does the <ViewTransition> sit?

              The view transitions quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does document.startViewTransition(update) do before it calls update?

                Choose an answer to see the explanation.

              2. Question 2 of 7In the step-by-step card example, which line does the console print first after a click?

                Read the code, then predictJavaScript
                const transition = document.startViewTransition(update);
                console.log("1. startViewTransition returned.");

                Choose an answer to see the explanation.

              3. Question 3 of 7You click Add one in this component. Does the paragraph animate?

                Read the code, then predictJSX
                function Cart() {
                  const [count, setCount] = useState(1);
                  return (
                    <>
                      <button onClick={() => setCount(count + 1)}>Add one</button>
                      <ViewTransition>
                        <p>{count} in the cart</p>
                      </ViewTransition>
                    </>
                  );
                }

                Choose an answer to see the explanation.

              4. Question 4 of 7goBack runs. Which class does the entering slide get?

                Read the code, then predictJSX
                function goBack() {
                  startTransition(() => {
                    addTransitionType("previous");
                    setIndex(index - 1);
                  });
                }
                
                <ViewTransition key={index} enter={{ next: "from-right", previous: "from-left" }}>
                  <p>{teas[index]}</p>
                </ViewTransition>

                Choose an answer to see the explanation.

              5. Question 5 of 7What goes wrong with this list of photos?

                Read the code, then predictJSX
                {teas.map((tea) => (
                  <ViewTransition key={tea.id} name="photo">
                    <img src={tea.photo} alt={tea.name} />
                  </ViewTransition>
                ))}

                Choose an answer to see the explanation.

              6. Question 6 of 7Details wrapped in <div><ViewTransition>…</ViewTransition></div> appear in a transition but don't animate. Why?

                Choose an answer to see the explanation.

              7. Question 7 of 7How do you respect people who ask their system for less motion?

                Choose an answer to see the explanation.

              Key takeaways

              • document.startViewTransition(update) takes a picture of the page, runs your update, and animates from the old picture to the new page. Its promises, updateCallbackDone, ready, and finished, follow the steps.
              • The animation is a tree of pseudo-elements: a group per name, with an old and a new picture. view-transition-name names a part, and CSS styles the pseudo-elements.
              • In React 19.3 and later, wrap part of the page in <ViewTransition>. React animates it only for transitions, Suspense reveals, and deferred values catching up, and never for urgent updates.
              • React picks enter, exit, update, or share. Choose your own classes with props, pick them by cause with addTransitionType, and pair two screens with a unique name.
              • Always add a prefers-reduced-motion rule, and keep a fallback for browsers without the API.

              Remember the one-liner.
              Mark the update as a transition, wrap what changes in a ViewTransition, and the browser animates from the old picture to the new page.

              Coming next: When React re-renders. It opens the Performance stage: what makes a component render again, and when that costs something.

              CompleteFrontend Clear concepts. Working examples.