The View Transitions API: Native Animated Page Changes
The View Transitions API brings smooth, app-like transitions to the web without a heavy SPA framework. Here's how it works and how to use it today.
For years, smooth page transitions were a reason to reach for a single-page application framework. The browser would do a hard cut between pages; frameworks intercepted navigation and animated between states with JavaScript. The View Transitions API gives you those animations natively — a few lines of CSS and JavaScript, no framework required.
How same-document transitions work
The API’s entry point for client-side transitions is document.startViewTransition(). You pass it a callback that makes your DOM changes, and the browser handles the animation:
document.startViewTransition(() => {
// Any DOM update goes here — swap content, update state, etc.
mainContent.innerHTML = newPageHTML;
});
What the browser does under the hood:
- Takes a screenshot (a snapshot) of the current state.
- Runs your callback to apply the DOM changes.
- Takes a snapshot of the new state.
- Crossfades between the two snapshots while the real DOM is already in its final state.
By default you get a smooth crossfade for free. The page appears to blend from the old state to the new one rather than cutting instantly.
The pseudo-element tree
When a transition runs, the browser creates a layered pseudo-element tree you can target with CSS:
::view-transition
└── ::view-transition-group(root)
├── ::view-transition-image-pair(root)
│ ├── ::view-transition-old(root) ← the snapshot of the old state
│ └── ::view-transition-new(root) ← the snapshot of the new state
The default crossfade animation is defined on ::view-transition-old and ::view-transition-new. You can override it entirely:
::view-transition-old(root) {
animation: 200ms ease-in both slide-out-left;
}
::view-transition-new(root) {
animation: 200ms ease-out both slide-in-right;
}
@keyframes slide-out-left {
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(100%); opacity: 0; }
}
Morphing specific elements with view-transition-name
The real power comes from animating individual elements between states. If the same element (a card, a heading, an image) exists in both the old and new view, you can tell the browser to morph it between positions:
.hero-image {
view-transition-name: hero;
}
Assign the same name to the corresponding element in the new view, and the browser creates a separate animation group for it — sliding, scaling, or crossfading that element independently of the rest of the page.
One important constraint: view-transition-name must be unique per captured snapshot. Two elements with the same name in the same state will cause the transition to skip for that element.
Cross-document transitions for MPAs
Same-document transitions require JavaScript, which works well for SPAs or any page that already manages its own navigation. But multi-page applications — plain HTML sites, server-rendered apps — can also get transitions without any JavaScript at all.
The @view-transition at-rule opts a page into cross-document transitions:
@view-transition {
navigation: auto;
}
Add this to both the outgoing and incoming page’s stylesheet. When the browser navigates between them (same origin), it captures snapshots and animates the transition using the same pseudo-element system. The default is a fade; you customize it with the same ::view-transition-* CSS selectors.
This means a static blog, a documentation site, or any server-rendered app can have smooth page transitions with a few lines of CSS and no client-side routing.
Respecting prefers-reduced-motion
Not everyone wants animated transitions. Users who have set the reduced-motion preference in their OS expect the web to respect it. Always wrap your transition customizations in a media query check:
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(root) {
animation: 200ms ease-in both slide-out-left;
}
::view-transition-new(root) {
animation: 200ms ease-out both slide-in-right;
}
}
Without this guard, you’re overriding an accessibility preference. The default crossfade is subtle enough that some teams leave it unrestricted, but custom animations that involve motion should always be gated.
Graceful degradation
document.startViewTransition is a modern API. On browsers that don’t support it, the call would throw. The correct pattern is a simple existence check:
function navigateTo(newHTML) {
if (!document.startViewTransition) {
// Fallback: instant DOM swap, no animation
mainContent.innerHTML = newHTML;
return;
}
document.startViewTransition(() => {
mainContent.innerHTML = newHTML;
});
}
The user experience degrades gracefully to an instant swap — exactly what happened before the API existed.
Framework support
You don’t have to wire this up manually in every project. Frameworks are adding View Transitions as first-class features. Astro, for instance, provides a <ViewTransitions /> component that handles cross-page transitions on top of its MPA architecture, giving you animated navigation with minimal configuration. For more on what Astro is doing in this space, see Astro 6: What’s New.
Browser support
View Transitions for same-document use shipped in Chromium and has been rolling out to other major engines. Cross-document transitions via @view-transition are newer and also shipping in Chromium-based browsers. Support is expanding — check current compatibility tables before relying on it without the fallback pattern above.
Transitions are a presentation concern, and poorly implemented animations can hurt performance. The Core Web Vitals guide covers how to measure rendering performance, and pairing View Transitions with container queries (see CSS Container Queries) lets you build components that look right wherever they land.
The takeaway
The View Transitions API closes a long-standing gap between native apps and the web. Same-document transitions work well for SPAs and partially-hydrated pages; cross-document transitions bring smooth navigation to traditional multi-page sites without any JavaScript overhead. The API is designed to degrade gracefully and the reduced-motion path is straightforward — there’s no good reason to skip it. If you’ve been avoiding SPA frameworks purely for their transition capabilities, this is worth a look.
Tagged
Keep reading
Takina · · 4 min read CSS object-fit and object-position, Explained
object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.
Takina · · 5 min read CSS inherit, initial, unset & revert Explained
CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.
Takina · · 5 min read Promise.all() vs allSettled() vs race() Compared
Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.