View Transitions in Next.js 16: the three layers
One is Baseline, another skips Firefox and the third is experimental in React. Knowing which one you use decides what ships to production.
- next.js
- react

Contents
- Layer 1: same-document, and why it is the one with no caveat
- Layer 2: cross-document, and what Firefox does with it
- Layer 3: React's <ViewTransition>
- The warning that changes the decision
- The six limitations of the React component
- The four recipes, and what each one communicates
- Four traps the docs warn about
- What I would use in production today
- Frequently asked questions
- Sources
"View Transitions" became the name of three different things, with three levels of maturity. Confusing them is what leads someone to ship an API the React documentation calls not production-ready.
| Layer | What it is | Status today |
|---|---|---|
document.startViewTransition() | Transition inside the same page | Baseline, Chrome 111+, Safari 18+, Firefox 144+ |
The @view-transition at-rule | Transition between pages (MPA) | Not Baseline, Firefox ignores it |
React <ViewTransition> component | Transition orchestrated by React | Experimental, Canary only |
Next.js 16 gives you access to all three, because the App Router runs React's latest Canary. That is convenient and dangerous in equal measure: using the component without noticing it is unstable is easy.
Layer 1: same-document, and why it is the one with no caveat
It is the only one of the three that passed into Baseline this year.
The API is a single function. You hand it a callback that changes the DOM, and the browser captures the before, applies the change, captures the after and animates between the two:
document.startViewTransition(() => {
// any DOM change here
list.replaceChildren(...newItems)
})Support today covers all three engines: Chrome 111+, Safari 18+ and Firefox 144+. No flag, no prefix.
Fine control comes through CSS, on the pseudo-elements the browser creates during the transition:
::view-transition-old(root) { animation: 0.4s ease-in both leave; }
::view-transition-new(root) { animation: 0.4s ease-in both arrive; }For a shared element transition (the thumbnail that becomes the large image), the mechanism is view-transition-name. Two elements in different states with the same name, and the browser interpolates between them.
This layer is where I would put anything that has to work in production today.
Layer 2: cross-document, and what Firefox does with it
Here the transition happens between pages, in the traditional navigation flow.
The opt-in is an at-rule, and it has to exist in both documents:
@view-transition {
navigation: auto;
}The navigation descriptor takes auto or none. With auto, the document goes through a transition when four conditions hold at once:
- The navigation is same-origin
- No cross-origin redirect sits in the path
- The
navigationTypeistraverse,pushorreplace - For
pushandreplace, the navigation was initiated by the user, not by the browser interface
That list explains most of the "it did not work" cases. A redirect passing through another domain takes the whole transition down, and navigation triggered by a browser button counts as nothing user-initiated.
And then there is Firefox. MDN classifies the at-rule as "Limited availability", outside Baseline. Firefox 144 supports the same-document API, and it ignores the @view-transition at-rule. A multi-page flow there never animates at all: navigation happens with the same hard cut as always.
That breaks nothing, it degrades. It is the difference between this layer and the next.
The same degradation reasoning holds for CSS scroll-driven animations: what degrades on its own is safe, what hides content needs a fallback.
Layer 3: React's <ViewTransition>
This is the one Next.js 16 delivers and the one demanding the most care.
The component wraps a subtree and animates when it enters, leaves or changes:
import { ViewTransition } from 'react'
<ViewTransition enter="auto" exit="auto" default="none">
<Video />
</ViewTransition>React activates the animation on its own, according to the kind of change:
| Prop | Fires when |
|---|---|
enter | The <ViewTransition> gets inserted inside a Transition |
exit | It gets removed inside a Transition |
update | A DOM mutation or layout change happens inside it |
share | A <ViewTransition> with the same name exists in the removed and the inserted tree |
default | Fallback for every trigger above |
The values are "auto", "none", a CSS class name, or an object with conditional logic, which allows a different animation for forward and backward navigation.
A shared element transition becomes declarative. Same name, different trees:
<ViewTransition name="cover"><div className="thumbnail" /></ViewTransition>
// ...on another screen
<ViewTransition name="cover"><div className="thumbnail fullscreen" /></ViewTransition>And an imperative path exists, for anyone wanting the Web Animations API instead of CSS. The onEnter, onExit, onUpdate and onShare callbacks receive an instance with the .old and .new pseudo-elements:
<ViewTransition
onEnter={(instance) => {
const anim = instance.new.animate(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 500 }
)
return () => anim.cancel()
}}
/>The warning that changes the decision
React's documentation is explicit about the stage:
Next.js 16 uses React's latest Canary in the App Router. That is why the component sits available to you with nothing installed, and it is why the warning is easy to miss. The availability comes from the framework; the stability does not.
What that means in practice: using <ViewTransition> today is accepting that the API can change in a minor release. For a client site under a maintenance contract, that is a debt you are taking on without saying so. For your own project or an experiment, it is acceptable.
Recording the other side is worth doing, because it is real: Vercel documents the component as a supported pattern, with an official guide of four recipes, and states that it works in the App Router with no configuration at all, so installing react@canary is unnecessary. An official skill even exists for teaching the recipes to an agent:
npx skills add vercel-labs/agent-skills --skill vercel-react-view-transitionsBoth things are true at once. React calls it experimental; Next.js delivers, documents and supports it. The risk decision is yours, and it should be explicit in the budget instead of implicit in the code.
If the transition has to exist and cannot break, layers 1 and 2 do the same job with CSS and the native API.
The six limitations of the React component
All documented, and five of them produce a symptom with no error.
1. It has to be the outermost element. The component works only if it sits before any DOM node. Wrapping a <div> around it breaks the enter and exit animations.
2. One per name, at a time. Only one <ViewTransition name="X"> can sit mounted at any moment.
3. It demands a Transition. Nothing happens outside startTransition(), <Suspense> or useDeferredValue. An ordinary state change activates no animation. In Next.js that is less restrictive than it sounds: route navigation is already a transition, so the animations activate on their own during navigation.
4. DOM only. It works in no React Native.
5. flushSync skips the animation. A synchronous update ignores the whole transition.
6. Accessibility is manual. React checks prefers-reduced-motion for nobody.
The sixth deserves attention. Unlike CSS, where you write the media query once and it holds, here the check is your responsibility:
const reduced = useMediaQuery('(prefers-reduced-motion: reduce)')
<ViewTransition default={reduced ? 'none' : 'auto'}>
<Content />
</ViewTransition>Without that, you are animating for someone who asked the system, in so many words, to stop animating.
The four recipes, and what each one communicates
The official guide organizes by meaning, not by technique. It is the best editorial decision Vercel made in that documentation, and the criterion is worth copying.
| Pattern | What it tells the user |
|---|---|
| Shared element morph | "Same thing, going deeper" |
| Suspense reveal | "The data arrived" |
| Directional slide | "Moving forward / going back" |
| Crossfade on the same route | "Same place, different content" |
The morph is the simplest and the most valuable: the same name on both sides, and the browser interpolates size and position.
// in the grid
<ViewTransition name={`photo-${photo.id}`}>
<Image src={photo.src} alt={photo.title} />
</ViewTransition>
// on the detail page
<ViewTransition name={`photo-${photo.id}`}>
<Image src={photo.src} alt={photo.title} fill />
</ViewTransition>No extra prop enters the picture. But a timing condition exists: the morph plays only when the destination renders in the same commit as the navigation, which happens with a prefetched page. If the destination lands in a Suspense fallback first, the pair never forms and the content arrives with the enter animation.
The directional slide is the one recipe depending on a Next.js-specific API. The transitionTypes prop on <Link> marks the navigation, and <ViewTransition> maps the type to the animation:
<Link href={`/photo/${photo.id}`} transitionTypes={['nav-forward']}>
<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"
>useRouter accepts transitionTypes in push() and replace() too. And the type is never automatic: you decide which links are "forward" and which are "back", according to your app's hierarchy.
Four traps the docs warn about
All of them produce a symptom with no error.
1. The wrapper in the layout never fires. Layouts persist between navigations, so enter and exit never happen there. The page's <ViewTransition> has to sit in each page.tsx.
2. default="none" without share kills the morph in silence. default="none" exists to stop a named <ViewTransition> from animating on every unrelated transition. But adding default="none" to a named pair without keeping the explicit share makes the pair stop morphing outright, with no warning.
3. Clicks during the transition get lost. The ::view-transition layer captures pointer events while animating. The fix is one line:
::view-transition { pointer-events: none; }Even so, hit-testing keeps skipping the named participants during the transition, so keep the animations short and avoid naming an element the user clicks in quick succession.
4. Going back through the browser carries no type. Navigation initiated by the back button or a swipe gesture carries no transition type, so the directional slide never plays. The shared element morph keeps holding, because it depends on the name alone.
Anchoring the header also pays off, since otherwise it slides along and the user loses the spatial reference:
::view-transition-group(site-header) { animation: none; z-index: 100; }
::view-transition-old(site-header) { display: none; }The display: none on the old snapshot prevents the flash of two headers visible at once.
What I would use in production today
The rule I follow has one line: use the most mature layer that solves the problem.
| You want | Use |
|---|---|
| Animating a content swap on the same page | Layer 1, startViewTransition() |
| A transition between pages on a multi-page site | Layer 2, @view-transition, accepting that Firefox never animates |
| A shared element between two App Router screens | Layer 3, if the project tolerates an unstable API |
| Reveal, parallax, progress bar | None of that, use scroll-driven animations |
And the fallback criterion, which holds for all three: how does it look if the transition never happens?
If the answer is "navigation gets a hard cut", everything is fine, since that is the behavior the web had for thirty years. If the answer is "the content stays invisible" or "the element sits in the wrong place", the problem has nothing to do with the transition; it is that you built the final state assuming the animation always runs.
That test is the one I apply to scroll-driven animations, and it fails the same kind of code: whatever starts from a hidden state.
If you have yet to update, what breaks in the Next.js 16 migration covers the other twenty changes that come along.
Frequently asked questions
Can I use View Transitions in production today?
The same-document layer, yes, with no caveat, since it is Baseline in all three engines. The cross-document one, yes, accepting that Firefox never animates and degrades to a hard cut. React's component, only if the project tolerates an API the documentation itself calls not production-ready.
Why does the transition between pages fail on my site?
Check layer 2's four conditions in order: same-origin, no cross-origin redirect, a compatible navigationType, and navigation initiated by the user. A redirect passing through another domain is the most common cause. After that, confirm the at-rule exists in both documents.
Does <ViewTransition> work without startTransition?
No. The component demands that the update happen inside a Transition, a <Suspense> or a useDeferredValue. An ordinary state change activates no animation, and no error points that out.
Does this replace GSAP for page transitions?
For a simple transition between states, yes, with no library. For a coordinated sequence of several stages, easing control per segment and synchronization with scroll, no. GSAP's license and scope cover when it stays the right tool.
Sources
- MDN — View Transition API. Accessed 20 August 2026.
- MDN —
@view-transition. Classified as Limited availability. Accessed 20 August 2026. - React —
<ViewTransition>. Canary/Experimental channel. Accessed 20 August 2026. - Next.js — Designing view transitions. Doc updated 7 August 2026. Accessed 20 August 2026.
- Next.js — How to upgrade to version 16. App Router on the React 19.2 Canary. Accessed 20 August 2026.
Verified on 20 August 2026.
Review trigger: revisit when (a) React's <ViewTransition> leaves the Canary channel and stabilizes, (b) Firefox starts honoring the @view-transition at-rule, moving cross-document into Baseline, or (c) the component's props change before stabilization, which the documentation itself warns can happen.
Read next
Motion •
Motion Design for the Web: The Complete Guide
Scroll, text, images and video: the complete catalog of motion techniques for the web, with implementation in Next.js and the cases where each one pays off.
- motion
- scroll
The definitive guide — a Next.js site built around motion and scroll
The scroll foundation that, when missing, keeps the animations from working at all: Lenis, GSAP and Next.js wired in the right order and the mistakes to avoid.
- next.js
- lenis
Infra •
Documentation: deploying a Next.js application with GitHub + Hostinger
Every push becomes a live site with no hosting panel involved: connecting GitHub to Hostinger, the build settings that break and the checks after each deploy.
- deploy
- github


