React 19.3 ships <ViewTransition>,
which animates a change by handing it to the browser’s View Transition API.
You can use it here. There is nothing to turn on, and no wrapper this package
provides — it is React’s component, used directly.
It has one boundary worth knowing before you reach for it, because the failure is silence: a transition that does not run looks exactly like one you styled badly.
| the change comes from | animates |
|---|---|
your own useState, inside startTransition |
yes |
| a server action’s result, put into state | yes |
<Form> — errors, success, optimistic updates |
yes |
a streamed <Suspense> boundary arriving |
yes, React does this on its own |
| navigating to another page | yes, behind a flag |
What works
Ordinary React. Give the changing element a key so React sees a replacement
rather than an edit, and commit inside startTransition:
'use client';
import { useState, startTransition, ViewTransition } from 'react';
import { addToTotal } from '../actions';
export function Total() {
const [total, setTotal] = useState<number | null>(null);
return (
<>
<ViewTransition>
<p key={String(total)}>{total ?? '—'}</p>
</ViewTransition>
<button
onClick={() =>
startTransition(async () => {
const next = await addToTotal(1);
startTransition(() => setTotal(next));
})
}
>
add
</button>
</>
);
}A server action is in the working column for a reason worth stating: its result
comes back as an ordinary return value, and what you do with it is useState.
That makes it a transition like any other. <Form> is built on useState
throughout, so everything it drives animates the same way.
Navigating between pages
Off by default, because it changes how every navigation commits:
rscKit({ viewTransitions: true })A build-time constant rather than a runtime setting, so an app that does not ask for it does not carry the boundary at all. What it animates is the segment a navigation replaces; what a page does inside itself needs no flag.
Coming back to a page you were just on
A navigation to a page still being held reveals it rather than refetching it, so the form you were filling in is still filled in. That applies to a link, not only the back button — having one keep your work and the other throw it away is a distinction nobody makes while using an app.
It is bounded, because the two halves pull against each other: what comes back is the tree from when you left, so its data is from then. Thirty seconds by default, which covers leaving a form to check something and coming straight back. Past that a link refetches.
import { setRevealWindow } from '@rsc-kit/core/navigate';
setRevealWindow(0); // never reveal — every link is a fresh requestThe back button is not bounded. It names a moment, and the page from that moment is the right answer however old it is.
Requirements
react and react-dom at 19.3 or newer. A scaffolded app pins ^19.2, so
this is an upgrade:
npm install react@^19.3 react-dom@^19.3React also documents a browser
api for rendering a component only in the browser — the right tool for a value
the server cannot know. It is not in 19.3 yet; see
static generation
for what to do meanwhile.