View Transitions
Date: 2026-08-17
The platform API for animating between two states of a page, or between two pages. It works by screenshotting the old state, letting you change the DOM, screenshotting the new one, and cross-fading between them — which means the animation you get for free is a fade, and anything more requires naming the elements that should be treated as the same thing.
The View Transitions API is a browser API that animates between two DOM states, within a page or across a navigation, by snapshotting each and animating pseudo-elements between them.
The mechanism
1 browser captures the CURRENT visual state
2 your callback runs — change the DOM however you like
3 browser captures the NEW state
4 it builds a tree of pseudo-elements and animates between them
5 when the animation finishes, the real DOM is shown
The DOM is not animated. Two snapshots are, which is why the transition is smooth even when the change was expensive — and why it can’t animate anything that happens during the change.
The smallest working form
// same-document: wrap the DOM change
document.startViewTransition(() => {
applyFilters(newFilters); // any DOM mutation, sync or awaited
});That alone gives a cross-fade between old and new. To animate specific elements as the same element, name them:
.product-hero { view-transition-name: product-image; }The name must be unique on the page at any moment. Two elements sharing a name means the transition silently doesn’t run — the commonest bug here, and it happens naturally on a listing page where every card has the same class.
// assign the name only to the one being transitioned
card.style.viewTransitionName = 'product-image';
document.startViewTransition(() => navigateToProduct(id))
.finished.finally(() => { card.style.viewTransitionName = ''; });That pattern — set the name, transition, clear it — is what makes a “click a product card, it grows into the product page hero” effect work.
Customising the animation
The browser generates pseudo-elements you can target with ordinary CSS animation:
::view-transition-old(product-image),
::view-transition-new(product-image) {
animation-duration: 300ms;
animation-timing-function: cubic-bezier(.2, 0, 0, 1);
}
/* the root cross-fade, if you want something other than a fade */
::view-transition-old(root) { animation: slide-out 200ms both; }
::view-transition-new(root) { animation: slide-in 200ms both; }Non-negotiable:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) { animation: none !important; }
}Motion between page states is exactly what that setting exists for, and a transition that ignores it can cause genuine discomfort — Reduced Motion.
Cross-document transitions
The more interesting case: transitions between real page navigations, on a multi-page site, with no JavaScript router.
@view-transition { navigation: auto; }Both pages opt in, and matching view-transition-name values on each side animate between them. This gives a server-rendered site the transition quality that was previously an argument for client-side routing — which is a meaningful shift, because it removes one of the few remaining reasons to adopt a single-page architecture for a content or commerce site — Rendering Strategies, Progressive Enhancement.
[CHECK: cross-document support and the exact opt-in syntax have moved during standardisation — verify current behaviour and browser support before shipping.]
The costs
- The page is frozen during the transition. Snapshots are static images; nothing is interactive until it finishes. Keep durations short — 200–300ms. A 600ms transition is 600ms of an unresponsive page, and it will show up in interaction metrics — Interaction to Next Paint
- Snapshotting a large or complex page costs time, on the main thread, before the animation starts
- Position is not preserved by default. An element that moves must be named, or it fades out in one place and in somewhere else
- Naming collisions fail silently, which makes debugging frustrating — check for duplicate names first, always
Feature detection
It degrades cleanly, and the pattern is short enough to have no excuse:
function update(fn) {
if (!document.startViewTransition) return fn(); // just do it
return document.startViewTransition(fn);
}Browsers without support get an instant change, which is a completely acceptable experience — the transition is decoration. Never make the DOM update conditional on the API existing — Browser Compatibility.
Where it’s worth using
- Product card → product page, the flagship case in commerce and genuinely effective at making a navigation feel instant
- Filter and sort changes on a listing page, where a fade masks the reflow
- Gallery and image switching on a product page
- Checkout steps, where a directional slide communicates progress
Where it isn’t: anything where the perceived speed comes from the content arriving, not from the animation. A transition on a page that then takes 800ms to load has added 300ms to the wait — Loading and Perceived Performance.
Where it interacts
- Reduced Motion — the accessibility constraint, and the one that isn’t optional
- Motion and Transitions — the design principles for when movement helps and when it’s noise
- Compositing and Layers — the rendering machinery underneath, and why snapshot-based animation is cheap to run
- The History API — same-document transitions usually accompany a
pushState, and both need the focus handling that a real navigation would have done