View Transitions in Next.js 16: Animating Between Pages Without a Library
React's ViewTransition component integrates with the browser's View Transitions API to animate route changes declaratively. Here's how to enable it in Next.js 16 and which three patterns cover most AI-native app use cases.
June 22, 2026 · 5 min read
Every route change in a traditional web app is a hard cut. Elements disappear and others appear with nothing to connect them. Animation libraries have filled this gap for years — tracking element positions, coordinating mount/unmount timing, managing z-index stacks — all for what amounts to a simple fade or morph. React's <ViewTransition> component changes the calculus entirely.
What the browser gives you
The View Transitions API lets the browser snapshot the old page, render the new one, and animate between the two snapshots. The browser owns the compositing; your code only needs to describe which elements should animate and how. React's <ViewTransition> component surfaces this as a declarative primitive. It activates during React Transitions — and in Next.js, every route navigation is already a Transition, so animations apply automatically during navigation.
Crucially, this is progressive enhancement. Browsers without View Transitions API support skip the animation and swap content normally. You do not need a feature-detect branch.
Enabling it in Next.js 16
Add one flag to your Next.js config:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
viewTransition: true,
},
}
export default nextConfigThen import the component from React — not from Next.js:
import { ViewTransition } from 'react'Pattern 1: Shared element morphing
The most impactful pattern. Give the same name prop to a <ViewTransition> on both the source and destination page. The browser animates size and position between the two automatically.
// app/results/page.tsx — the list
<Link href={`/result/${item.id}`}>
<ViewTransition name={`result-card-${item.id}`}>
<ResultCard item={item} />
</ViewTransition>
</Link>
// app/result/[id]/page.tsx — the detail
<ViewTransition name={`result-card-${item.id}`}>
<ResultDetail item={item} />
</ViewTransition>In an AI-native app this pattern maps well to search results: the user submits a query, sees a list of generated answers, clicks one to expand it. The card morphs into the detail view. The user never loses the spatial relationship between the selection and the result.
Pattern 2: Suspense reveals
LLM calls are slow. Pages that fetch AI-generated content show a skeleton while data loads. Without a transition, the skeleton snaps to real content. With <ViewTransition>, you can slide the skeleton out and the content in:
import { Suspense, ViewTransition } from 'react'
export default async function Page() {
return (
<Suspense
fallback={
<ViewTransition exit="slide-down">
<ContentSkeleton />
</ViewTransition>
}
>
<ViewTransition enter="slide-up" default="none">
<AIContent />
</ViewTransition>
</Suspense>
)
}The default="none" prop is important: it prevents this component from animating during unrelated transitions like shared element morphs elsewhere on the page.
Pattern 3: Directional slides
For multi-step flows — onboarding wizards, chat history, paginated results — directional motion tells the user where they are in a sequence. Tag <Link> with a transitionTypes prop, then map those types to animations on the page wrapper:
// Forward navigation
<Link href="/step/2" transitionTypes={['nav-forward']}>
Next
</Link>
// Back navigation
<Link href="/step/1" transitionTypes={['nav-back']}>
Back
</Link>
// Page layout
<ViewTransition
enter={{ 'nav-forward': 'slide-from-right', 'nav-back': 'slide-from-left', default: 'none' }}
exit={{ 'nav-forward': 'slide-to-left', 'nav-back': 'slide-to-right', default: 'none' }}
default="none"
>
{children}
</ViewTransition>The transition type is not inferred — you decide which links are forward and which are back based on your app's hierarchy. This is intentional: the API does not assume a navigation model.
Reduced motion
Directional slides move content across the viewport and are the primary trigger for motion sensitivity. The safest global rule is to zero out durations under prefers-reduced-motion: reduce:
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*),
::view-transition-group(*) {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
}Content still swaps; it just does so instantly rather than animating. Opacity-only transitions carry less risk and can be preserved for users who want some feedback but not movement.
What this means in practice
For AI-native apps, view transitions solve a real problem: users wait for LLM responses, and skeleton-to-content swaps are frequent. A well-timed Suspense reveal makes a 1.2-second LLM call feel deliberate rather than broken. Shared element morphing makes the connection between a search result card and its detail page obvious. Directional slides give multi-step AI workflows a physical analogue — turning pages rather than jumping between disconnected screens.
The library-free angle is worth repeating: zero JS animation runtime, no layout measurement, no FLIP gymnastics. The browser handles compositing at 60fps natively. For teams shipping AI-native products where bundle size and initial load time already face pressure from model SDKs and streaming infrastructure, that trade-off is straightforward.