TL;DR
- React 19.3 (9 September 2026) made
<ViewTransition>stable, and Next.js 16.4 ships it. In the App Router every navigation is a Transition, so boundaries animate with no extra code. - An unconfigured boundary animates on every trigger. Start from
default="none"and opt into the triggers you want. - Follow the Suspense rule: fallbacks appear instantly, the reveal animates, cached content never animates.
update="auto" default="none"around<Suspense>does all three. - A DOM node above a boundary, a boundary in a layout, or a shared pair without
shareall disable animations silently. - Reduced motion and clicks during animations are your responsibility, not React's.
Most of the View Transition bugs that ship in React apps over the next year will come from animating too much, not too little. React 19.3 made <ViewTransition> stable, Next.js 16.4 bundles it, and the default behaviour is generous: an unconfigured boundary cross-fades on every Transition that touches it. That is pleasant in a demo and tiring in a product. The teams that get this right will treat animation as something each boundary opts into, starting from default="none", and will follow one rule for loading states that the React team spelled out in the 19.3 announcement.
This guide takes that position and builds on it. It covers what became stable and when, which updates can trigger an animation at all, the Suspense rule, shared elements and directional navigation in Next.js, the placement mistakes that switch animations off without an error, and the accessibility and browser-support work that remains your responsibility. It ends with the checks we run on a codebase before View Transitions go to production.
Every API detail below was checked on 9 November 2026 against the React 19.3 release post and reference docs, the Next.js 16.4 release post and view transitions guide, the documentation bundled inside the next@16.4.5 package, and the npm registry. Where two official sources disagree, the section says so.
What React 19.3 made stable, and which versions you need
React 19.3.0 was published to npm on 9 September 2026, and the React 19.3 announcement moved two APIs out of the experimental channel: <ViewTransition> and Fragment refs. Both had been previewed as experimental the year before. The current patch at the time of writing is 19.3.1, published on 14 October 2026, and it is what the latest tag on npm resolves to.
The release also added addTransitionType support for typed animations, a browser() API in react-dom for opting components out of server rendering, Trusted Types support, and the ability for Server Components to render a Context directly. Nothing in the release is listed as a breaking change, which is consistent with it shipping as a minor version rather than React 20.
For Next.js projects, the version that matters is 16.4. The Next.js 16.4 release post, published on 6 October 2026, states that it ships with React 19.3, including stable View Transitions and Fragment refs. If you are upgrading for this reason, go straight to the current patch rather than 16.4.0. On 3 November the team published a security release as 16.4.5 and 15.5.29, fixing one high and several medium severity issues, most of them in Cache Components. If your project is still on Next.js 15, our Next.js 16 upgrade checklist covers the breaking changes to handle first.
| Package | Version checked | Published | Why it matters |
|---|---|---|---|
react, react-dom
|
19.3.1 | 14 October 2026 | Stable <ViewTransition>, Fragment refs, addTransitionType
|
next |
16.4.5 | 3 November 2026 | Ships React 19.3, transitionTypes on <Link>, security fixes |
Keep react and react-dom on the same version. A mismatch is one of the quicker ways to lose an afternoon, because npm refuses the install and forcing it produces runtime errors that look unrelated to the upgrade.
Which updates can trigger a View Transition
A View Transition only runs when an update is marked as non-urgent. The <ViewTransition> reference names three sources: state updates inside startTransition, a <Suspense> boundary revealing its content, and an update coming from useDeferredValue. A plain setState in an event handler never animates, however the boundary is configured.
That design is deliberate, and it is worth understanding before writing any CSS. Urgent updates, such as typing into an input or toggling a checkbox, should appear immediately, and React will not put an animation between the user and the result. Non-urgent updates are the ones where React is already willing to wait, so adding a short animation does not change how responsive the interface feels.
import { ViewTransition, useState, startTransition } from 'react';
function Panel() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => startTransition(() => setOpen(o => !o))}>
Toggle details
</button>
{open && (
<ViewTransition>
<Details />
</ViewTransition>
)}
</>
);
}
Remove startTransition from that click handler and the panel still opens, but without animation. When someone reports that "the animation only works sometimes", this is the first thing to check: one code path wraps the update in a Transition and another does not.
In the Next.js App Router, route navigations are already Transitions, so <ViewTransition> boundaries animate during navigation without any extra code. The Next.js view transitions guide makes this point directly, and it is the reason a single unconfigured boundary in a shared component can start animating on every page change.
You also never call document.startViewTransition() yourself. React calls it for you, and calling it alongside React's own handling produces competing transitions.
The four triggers: enter, exit, update and share
React picks an animation based on how the tree changed around each boundary. There are four triggers, and each one can be configured separately:
-
enter runs when the
<ViewTransition>is inserted during a Transition. - exit runs when it is removed.
- update runs when DOM inside it changes, or when its size or position changes because of an immediate sibling.
- share runs when a named boundary is removed in one place and a boundary with the same name is inserted somewhere else.
Each trigger prop accepts "auto" (the browser's default cross-fade), "none", a class name of your own, or an object keyed by transition type. A class name is the usual choice, because it connects the boundary to CSS through the view transition pseudo-elements:
<ViewTransition enter="slide-up" exit="fade-out">
<Notice />
</ViewTransition>
::view-transition-new(.slide-up) {
animation: 220ms ease-out both slide-up;
}
::view-transition-old(.fade-out) {
animation: 150ms ease-in both fade-out;
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-out {
to { opacity: 0; }
}
The React docs recommend styling through these classes rather than targeting view-transition-name values directly. Classes survive refactors and work with React's generated names; hard-coded names do not.
There is also an imperative route. The event props onEnter, onExit, onUpdate and onShare receive the transition instance and the active types, so you can drive the animation with the Web Animations API and return a cleanup function. Only one event fires per boundary per Transition, and onShare wins over onEnter and onExit. Most interfaces never need this, and CSS keeps the animation where designers can find it.
One more detail matters for layouts: React treats each boundary's content as a single image that moves, scales and cross-fades. Child elements inside it do not animate their own positions. If a card's title and image should move independently, they need separate boundaries.
Why default="none" should be your starting point
default="none" turns off every trigger on a boundary except the ones you list explicitly. It is the single most useful prop on <ViewTransition>, and starting from it is the opt-in model this guide argues for.
The problem it solves is not obvious until an app has more than a few boundaries. An unconfigured boundary uses "auto" for every trigger, so it cross-fades whenever any Transition updates it. In the App Router that means every navigation, every router.refresh(), and every Suspense reveal nearby. The Next.js guide puts it plainly: without default="none", every named <ViewTransition> animates whenever any transition runs on the page. Individually, each cross-fade is subtle. Together, they make an interface feel slow even when the data arrives quickly, because the user watches content dissolve rather than appear.
<ViewTransition default="none" enter="slide-up">
<SearchResults query={query} />
</ViewTransition>
That boundary animates when the results first enter and stays still for everything else. If you later decide that updates should cross-fade too, you add update="auto", and the change is visible in code review instead of being an accident of defaults.
There is one trap. A named pair used for a shared element transition needs an explicit share prop once you add default="none". The Next.js guide warns that with default="none" and no share, the pair silently stops morphing. Nothing errors, and the morph simply disappears.
<ViewTransition name={`product-${id}`} share="morph" default="none">
<ProductImage id={id} />
</ViewTransition>
As a working rule, we treat any <ViewTransition> without default="none" as something a reviewer should question. Sometimes the generous default is right, for a small, self-contained widget that only ever changes through one Transition. Usually it is a boundary that nobody has fully thought through yet.
The Suspense rule: instant fallbacks, animated reveals
The React 19.3 announcement gives three principles for animating around Suspense, and they are the core of this guide:
- Fallbacks should appear immediately, without animation.
- A fallback should change to its final content with animation.
- Children that do not suspend should appear immediately, without animation.
Each principle protects responsiveness in a different way. An instant fallback tells the user that their click registered; fading it in delays that acknowledgement for no benefit. An animated reveal smooths the jump from skeleton to content, which is the moment where a hard cut feels most jarring. And content that is already cached should never animate, because an animation on data the user has seen before turns an instant response into a slow one.
The default configuration breaks the first and third principles. Wrap a Suspense boundary in a plain <ViewTransition> and the fallback fades in, then content that was already loaded animates again every time the component is shown. The announcement's fix is to switch off everything except updates:
<ViewTransition update="auto" default="none">
<Suspense fallback={<OrderSummarySkeleton />}>
<OrderSummary orderId={orderId} />
</Suspense>
</ViewTransition>
Now the skeleton appears at once, the change from skeleton to summary cross-fades, and once the summary is cached, toggling it is instant. This is a small change with a large effect on how an app feels, and it is the pattern we would put in a team's shared components before anything more decorative.
Suspense boundaries also wait for more than data. Inside a <ViewTransition>, React waits up to 500 milliseconds for fonts and visible images to load before running the animation, and the 19.3 post shows how wrapping images and stylesheets inside a boundary lets them take part in Suspense. That turns the familiar flicker of an image arriving late into a coordinated reveal, as long as the image loads within that window.
Choosing between the update and enter/exit patterns for Suspense
There are two ways to animate a Suspense reveal, and they communicate different things.
The update pattern wraps the whole Suspense boundary in one <ViewTransition>. The fallback and the content share one identity, so React cross-fades from one to the other in place. It suits content that occupies the same space as its skeleton, such as a card, a table or a summary panel.
The enter and exit pattern puts one boundary around the fallback and another around the content. The fallback exits and the content enters, each with its own animation:
<Suspense
fallback={
<ViewTransition exit="slide-down" default="none">
<ArticleSkeleton />
</ViewTransition>
}
>
<ViewTransition enter="slide-up" default="none">
<Article slug={slug} />
</ViewTransition>
</Suspense>
This is the pattern the Next.js guide uses for a detail page, with deliberately uneven timing: the skeleton leaves in about 150 milliseconds, and the content's fade waits for that exit to finish before arriving over roughly 210 milliseconds, while its slide runs for 400. The reasoning is sound. Old content should leave quickly so that it does not compete for attention, and new content should arrive gently enough for the eye to register it.
| Situation | Pattern | Why |
|---|---|---|
| Skeleton and content occupy the same box | Update, one boundary around <Suspense>
|
Reads as the same thing becoming ready |
| Content is a different shape from its skeleton | Enter and exit, one boundary each | Avoids stretching one image into another |
| Content is usually cached | Either, with default="none"
|
Cached reveals should not animate |
| Many small boundaries on one screen | Update, or none at all | Several entrances at once looks busy |
If you are unsure, start with the update pattern and default="none". It is the quieter of the two and the harder one to get wrong.
Shared element morphs and the prefetch condition
A shared element transition makes one element appear to move between two states: a product thumbnail growing into the hero image on its detail page, for example. You create one by giving two boundaries the same name, one in the old tree and one in the new:
// In the grid
<Link href={`/products/${p.id}`}>
<ViewTransition name={`product-${p.id}`} share="morph" default="none">
<Image src={p.thumb} alt={p.title} width={320} height={320} />
</ViewTransition>
</Link>
// On the detail page
<ViewTransition name={`product-${p.id}`} share="morph" default="none">
<Image src={p.hero} alt={p.title} width={960} height={960} />
</ViewTransition>
Three conditions decide whether the morph actually plays, and each one fails silently.
Names must be unique across the whole app at any moment. Only one boundary with a given name can be mounted at a time. A list where every item uses the same name raises an error about two components sharing a name, which is the easy case. The harder case is a name that collides only when two specific components happen to be mounted together. Build names from a stable identifier, and keep any fixed names in a module of namespaced constants.
Both sides must be in the viewport. If either element is off screen, React does not form a pair, and each side runs an ordinary enter or exit animation instead.
The destination must render in the same commit. This is the condition that catches Next.js teams. The guide explains that the morph plays when the destination content renders in the same commit as the navigation, which is the case for prefetched pages. If the destination suspends into a fallback first, there is no pair, and the content uses its enter animation when it arrives. In practice, a shared element morph is only as reliable as your prefetching. If the detail page fetches uncached data before it can render the hero image, the morph will rarely play, and the fix belongs in your data and caching strategy, not in CSS.
The Next.js guide also adds a short blur to the image pair during the morph. It is a useful trick, because it hides the scaling artefacts that appear when a small image is stretched to a larger size mid-flight.
Directional navigation with transition types in Next.js 16.4
Transition types let one boundary choose different animations depending on why a Transition happened. In plain React you add a type inside the Transition with addTransitionType, for example addTransitionType('next'). In Next.js, <Link> accepts a transitionTypes prop, and the bundled documentation for 16.4.5 records it as added in version 16.2.0. useRouter supports the same option on push() and replace().
<Link href={`/orders/${order.id}`} transitionTypes={['nav-forward']}>
View order
</Link>
<Link href="/orders" transitionTypes={['nav-back']}>
Back to orders
</Link>
Each page then maps those types to animations, with everything else switched off:
<ViewTransition
enter={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
default="none"
>
<OrdersTable />
</ViewTransition>
Two details from the Next.js guide decide whether this works. First, put the wrapper in each page.tsx, not in a layout. Layouts persist across navigations, so their boundaries never enter or exit and the directional animation never fires. Second, decide which links count as forward and which as back based on your navigation hierarchy, because Next.js does not infer it. The guide's convention, with forward content sliding in from the right and back content from the left, matches how readers of left-to-right languages expect pages to turn.
Directional slides also need a fixed reference point. If the header slides with the content, the user loses track of what moved. The guide gives the header its own viewTransitionName and disables its animation, so the header stays put while the page content moves beneath it. And it adds pointer-events: none to the ::view-transition overlay, because without it, clicks made during an animation are swallowed by the overlay instead of reaching the page.
::view-transition {
pointer-events: none;
}
That one rule is easy to miss and fixes a whole class of "the button didn't respond" reports.
A Next.js configuration detail where the docs disagree
Whether Next.js needs a configuration flag for View Transitions depends on which page of its documentation you read. The hosted reference page for experimental.viewTransition, still labelled for version 16.2.3, says to enable the flag in next.config.js and warns that the feature is experimental and not recommended for production. The view transitions guide, last updated on 15 October 2026, says that view transitions work in the App Router with no configuration and that React 19.3 provides everything it uses.
The documentation bundled inside the next@16.4.5 package settles it for current versions. That package contains the guide with the "no configuration" wording and no longer includes a reference page for the viewTransition flag at all. On 16.4 and later, there is nothing to switch on. On 16.2 or 16.3, follow the flag documentation for your installed version, which you can read in node_modules/next/dist/docs. That folder is a reliable tiebreaker whenever the hosted docs and your installed version seem to disagree.
Placement mistakes that silently disable animations
Most "my View Transition does nothing" problems come from where the boundary sits in the tree, not from its props. None of these produces an error.
A DOM node above the boundary. Enter and exit only activate when the <ViewTransition> comes before any DOM node in the part of the tree being inserted or removed. Wrapping it in a <div> breaks it. The fix is to move the boundary outside the element:
// Does not animate on enter or exit
<div className="card">
<ViewTransition><Card /></ViewTransition>
</div>
// Animates
<ViewTransition>
<div className="card"><Card /></div>
</ViewTransition>
Boundaries inside layouts. As above, layouts persist, so enter and exit never fire there. Directional and page-level animations belong in pages.
Wrappers inside lists. Nested boundaries receive their own mutations rather than passing them to the parent, and in a list, the update animation applies to each item only when its <ViewTransition> sits directly in the list rather than inside a wrapper element. Reordering also needs stable keys. An index used as a key makes React believe every item changed.
Unstable keys on keyed swaps. The Next.js guide's tab example relies on key={slug} to make React treat old and new content as an exit and enter pair. If the key does not change when the content does, React performs an update in place instead.
This is also where Fragment refs help indirectly. A common reason for a wrapper <div> above a boundary is that some other code needed a DOM node to attach an IntersectionObserver or an event listener. In React 19.3, a ref on a <Fragment> gives you a FragmentInstance with observeUsing, addEventListener, focus and measurement methods that act on the fragment's children as a group, without adding a node. The <Fragment> reference covers the full method list. Removing those wrapper elements is often what lets a boundary sit at the top of its subtree, where it needs to be.
Back navigation, interruptions and flushSync
Three runtime behaviours explain the remaining cases where an animation should play and does not.
Browser back and forward. The React reference says React skips animations for popstate navigations, because scroll and form restoration have to finish synchronously, and notes that routers built on the Navigation API avoid the limitation. The Next.js guide describes the result in its own router slightly differently: back and forward gestures carry no transition type, so directional slides do not play, while a shared element morph still applies if both pages have matching names. The two descriptions are about different layers, but they leave the same advice. Never rely on an animation to explain a back navigation, and test the back button and swipe gestures on each browser you support.
A transition already running. If one View Transition is in progress, React waits for it to finish, and updates that arrive in the meantime are batched. A long animation therefore delays the next one. This is the practical case for short durations: the Next.js guide's timings sit between 150 and 400 milliseconds, and we would treat anything longer on a frequently used screen as a design decision that needs a reason.
flushSync. A flushSync call in the middle of the sequence makes React skip the Transition. Third-party components that call it internally can disable animations in a way that is hard to trace, so when an animation stops working after a library upgrade, search the dependency for it.
Reduced motion and accessibility remain your job
React does not disable View Transitions for users who have asked their operating system to reduce motion. The reference docs recommend always handling the prefers-reduced-motion media query, and the Next.js guide supplies a blunt but effective default:
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*),
::view-transition-group(*) {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
}
With this in place, content swaps instantly, which is what the browser does without view transitions at all. A finer approach keeps cross-fades and removes positional movement, since large horizontal slides are the most likely to cause discomfort, while opacity changes carry far less risk. Whichever you choose, ship the media query in the same pull request as the first animation, not as a follow-up.
The overlay matters for keyboard and pointer users too. Named participants, such as an anchored header, are skipped by hit-testing for the duration of a transition even with pointer-events: none on the overlay, so avoid naming elements that people click quickly and repeatedly. And because animations wait for Transitions, focus management should not depend on an animation finishing: move focus as part of the state change, not in an onEnter callback.
If your products serve customers in the EU, motion and focus behaviour are part of the accessibility picture your team is already assessing, and our guide to European Accessibility Act compliance covers where those obligations come from.
Browser support and progressive enhancement
Same-document view transitions reached Baseline status on 14 October 2025, when Firefox 144 shipped them and all three major browser engines supported the core API. web.dev's announcement also noted that Firefox's first implementation did not include view transition types, which React's integration depends on for typed animations.
The Next.js guide, updated in October 2026, says React's integration uses the newer parts of the API, namely transition types and view-transition-class, and lists them as available in Chromium 125 and later and in recent Safari and Firefox versions, with a warning that some animations may behave differently in Safari. Check the current support tables on the day you ship rather than relying on a version number in an article, this one included. The MDN View Transition API documentation links to them.
The reassuring part is the failure mode. Without browser support, the application works normally and the transitions simply do not animate. View Transitions are progressive enhancement by design, so the risk in an unsupported browser is a plainer experience, not a broken one. That is also the right way to brief a client or product owner: animation is a refinement on top of a working interface, and no user flow should depend on it.
Two adjacent limits are worth knowing. <ViewTransition> works only in the DOM today; React Native support is in progress but not available. And because the browser animates snapshots of the old and new content, a transition that covers the whole page asks more of a low-end phone than one that covers a single card. Animate the element that changed, not the whole page, and measure the screens that matter with real devices. If interaction latency is already a concern on your mobile templates, our piece on mobile-first design and Core Web Vitals is the right place to start before adding motion.
Should you replace your animation library?
For route transitions, loading reveals and simple enter and exit effects, <ViewTransition> is now a credible replacement for a JavaScript animation library, and it has two advantages a library cannot easily match. It is coordinated with React's own scheduling, so animations line up with Suspense and Transitions instead of fighting them. And it runs on a browser API, so there is less JavaScript to download for those effects.
It is not a replacement for everything. Gesture-driven motion, spring physics, scroll-linked effects and animations that must respond continuously to input are still better served by a dedicated library or by CSS scroll-driven animations. That is our view rather than an official position, and it reflects what each tool is designed for: View Transitions animate between two states, while those effects describe motion that has no fixed end state.
A sensible migration moves one category at a time. Route transitions first, because the App Router already treats navigations as Transitions. Suspense reveals second, using the rule above. Leave interactive motion where it is until there is a specific reason to change it.
A test plan before shipping View Transitions
A View Transition that looks right on a fast laptop in Chrome has passed the easiest test available. Before switching them on in production, we work through these checks on every screen that uses them:
-
Confirm versions.
npm ls react react-dom nextshows matching React versions at 19.3.1 or later, and Next.js at 16.4.5 or later. -
List every boundary. Search the codebase for
<ViewTransitionand record which triggers each one animates. Anything withoutdefault="none"needs a reason. - Check placement. For each boundary that should enter or exit, confirm no DOM node sits above it in the inserted subtree, and that page-level boundaries live in pages rather than layouts.
- Test the Suspense rule. Throttle the network in DevTools and confirm fallbacks appear instantly, reveals animate, and revisiting cached content does not animate.
- Test shared elements cold and warm. Navigate with an empty cache and with prefetched data. The morph should play when prefetched, and fall back to a clean enter animation when not.
- Click during animations. Confirm buttons and links respond while a transition is running.
- Use the back button and gestures on every supported browser, and confirm nothing depends on an animation to make sense.
- Turn on reduced motion in the operating system and confirm positional movement stops.
- Test the browsers you support, including at least one where typed animations may be missing, and confirm the interface still works without animation.
- Check keyboard focus after every animated state change.
Each of these, with pass criteria and a readiness score, is in the printable audit worksheet below, so the checks can be split across a team and ticked off screen by screen.
Get the React View Transitions Audit Worksheet
A two-page printable worksheet with 20 checks across setup, behaviour and release, each with clear pass criteria, plus a readiness score that tells you whether a screen is ready for View Transitions in production. No account, no sales sequence.
FAQ
Do I need a configuration flag to use ViewTransition in Next.js?
Not on Next.js 16.4 or later. The view transitions guide and the documentation bundled with next@16.4.5 say view transitions work in the App Router with no configuration, because React 19.3 provides <ViewTransition> and addTransitionType. Older 16.x versions documented an experimental.viewTransition flag, so check the docs in node_modules/next/dist/docs for your installed version.
Why does my ViewTransition not animate?
The most common causes are an update that is not wrapped in startTransition, a DOM element placed above the boundary, a boundary inside a layout that never enters or exits, and default="none" on a shared pair without an explicit share prop. A flushSync call during the update also makes React skip the animation. None of these produces an error.
Does React ViewTransition work in Firefox and Safari?
The core View Transitions API has been supported in all major browser engines since October 2025. React's integration also uses transition types and view-transition-class, which Next.js lists as available in Chromium 125 and later and in recent Safari and Firefox versions. Where support is missing, the app works normally and simply does not animate.
Does React turn off View Transitions for reduced motion?
No. React does not check the user's motion preference. Add a prefers-reduced-motion media query that sets animation durations and delays to zero on the ::view-transition-old, ::view-transition-new and ::view-transition-group pseudo-elements, or keep only opacity changes and remove movement.
Can I use ViewTransition with React Native?
Not yet. The React 19.3 announcement says <ViewTransition> currently works only in the DOM, and that support for React Native and other platforms is being worked on. Fragment refs and the other 19.3 changes are separate from this limitation.
Originally published on zignites.com.
Top comments (0)