View Transitions API — SPA smoothness in a multi-page site.
For years, the only way to get smooth cross-page transitions on the web was to abandon multi-page architecture entirely and ship a JavaScript SPA. The View Transitions API ends that trade-off. Same-document transitions are in every browser. Cross-document transitions landed in Chrome 126 and Safari 18. Here's how to use them in production — and why most tutorials get it wrong.
01What the API actually does
When you call document.startViewTransition(), the browser does four things, in order:
- Takes a screenshot of the current page state
- Runs your callback (which mutates the DOM)
- Takes a screenshot of the new state
- Crossfades the two screenshots while the new DOM is shown underneath
The default transition is a 250ms opacity crossfade. That's it. No setup, no animation engine, no library. You wrap your DOM mutation in a function, hand it to the browser, and the browser handles the visual interpolation. Everything else builds on this primitive.
02Same-document transitions — the easy case
On a single page where you're toggling between views (filtering a list, opening a modal, switching tabs), the API is one function call:
function switchView(newView) {
// Feature detect; fall back to no transition on Firefox
if (!document.startViewTransition) {
updateDOM(newView);
return;
}
document.startViewTransition(() => {
updateDOM(newView);
});
}
That's a complete implementation. The browser takes care of capturing old state, running your update, capturing new state, and crossfading. Zero animation code in your application.
03Named transitions — the magic part
Where the API gets interesting is view-transition-name. When you give an element a unique name, the browser tracks that element across the transition. If the element exists before and after, it morphs from old position/size to new position/size — instead of crossfading.
/* In your list view */
.card[data-id="42"] .thumbnail {
view-transition-name: thumb-42;
}
/* In your detail view (same name on the hero image) */
.detail-hero img {
view-transition-name: thumb-42;
}
Now when the user clicks the card and you navigate to the detail view, the thumbnail animates from its position in the grid to its new position as the hero image. The browser interpolates size, position, border-radius — everything. You wrote zero JavaScript animation code.
view-transition-name must be unique on the page at any given moment. Two elements with thumb-42 simultaneously will throw an error. Generate the names dynamically from your data IDs — that's the whole point.04Cross-document transitions — the big one
Same-page transitions are useful. But the real prize is cross-document — transitions that work when navigating between separate HTML pages. As of Chrome 126 and Safari 18, this is supported with a single CSS opt-in:
@view-transition {
navigation: auto;
}
Add this rule to every page where you want transitions. Now any standard <a href> click between two same-origin pages with this rule will trigger a view transition automatically. No JavaScript. No router. The browser handles the navigation and the transition together.
This is genuinely new. Before, the "fast, smooth navigation" UX was exclusive to JavaScript SPAs. Now any server-rendered site can have it.
05Customizing the transition
The default crossfade is fine for prototyping but boring in production. The transition is driven by two pseudo-element trees: ::view-transition-old(name) for the outgoing state and ::view-transition-new(name) for the incoming state. Both are children of ::view-transition-group(name), which itself is wrapped by ::view-transition-image-pair(name).
@keyframes slide-from-right {
0% { transform: translateX(30px); opacity: 0; }
}
@keyframes slide-to-left {
100% { transform: translateX(-30px); opacity: 0; }
}
/* Default root transition uses 'root' as the implicit name */
::view-transition-old(root) {
animation: slide-to-left 300ms ease forwards;
}
::view-transition-new(root) {
animation: slide-from-right 300ms ease forwards;
}
You're animating CSS pseudo-elements with keyframes — the same API you've used for years. The only new thing is the pseudo-element tree.
06Directional transitions — back vs forward
A common need: when going from the list to a detail page, slide left. When going back, slide right. The API doesn't give you direction directly. You set it yourself before triggering the transition, using a data attribute on the root:
// JS: set direction before navigating
document.documentElement.dataset.transition = 'forward';
location.href = '/detail/42';
// On back navigation, set 'backward' via the navigation API
navigation.addEventListener('navigate', e => {
if (e.navigationType === 'traverse') {
document.documentElement.dataset.transition = 'backward';
}
});
Then use the data attribute as a CSS selector to pick the appropriate animation:
html[data-transition="forward"] ::view-transition-old(root) {
animation: slide-to-left 300ms ease forwards;
}
html[data-transition="backward"] ::view-transition-old(root) {
animation: slide-to-right 300ms ease forwards;
}
07Gotchas in production
The transition blocks paint. Between the screenshot of the old state and the new render, the browser pauses. Long-running callbacks inside startViewTransition will visibly freeze the page. Keep DOM mutations fast.
Animations run on the GPU snapshot, not the live DOM. During the transition, what you see is a screenshot, not the actual elements. Hover states, video playback, form focus — none of those work mid-transition. Keep transitions under 400ms.
Cross-document transitions require same-origin. The two pages must share scheme, host, and port. You can't transition from example.com to app.example.com without configuring the Subresource Loading API, and even that's experimental.
view-transition-name doesn't inherit. Setting it on a parent doesn't apply to children. Each element that participates needs its own name.
Forms break. A user typing in an input when a transition fires will lose focus and possibly text. Disable transitions on routes that involve form state, or save and restore focus manually.
08When to use it (and when not to)
Use View Transitions for:
- List → detail navigations (hero image morphing is the canonical use case)
- Tab switches and filter changes
- Modal open/close on small screens
- Same-origin cross-page navigation where you control both ends
Don't use it for:
- External links (the browser handles those)
- Pages that submit forms or process payments mid-transition
- Routes where transition direction is ambiguous
- Very simple state changes that don't benefit from animation
∞The framework-killer angle
Half the reason teams reach for React or Vue is to get smooth transitions between views. The View Transitions API removes that justification for most use cases. A server-rendered site with this API can match the feel of a JavaScript SPA — without the bundle size, the hydration cost, or the SEO compromises.
That doesn't mean every site should drop frameworks. But it means the default math has changed. Before, transitions cost you a framework. Now they cost you eight lines of CSS. That's a different decision.