View Transitions (Cross-Doc)
Browser-native page morphs. Same-doc and across documents.
For years, native apps had silky animated transitions between screens, and the web had... a hard cut. The View Transitions API finally gives browsers a single primitive for animating DOM changes, including across full page navigations. Two lines of code can turn a jarring swap into a Hollywood-grade morph. And as of 2025, all major browsers support same-document transitions, with cross-document close behind.
The core idea: snapshot, swap, animate
Under the hood the browser does three things when you start a view transition:
- Snapshot the current page as a still image (the old state).
- Run your DOM mutation callback. Snapshot the result (the new state).
- Animate from old to new using CSS pseudo-elements that you can target and customize.
It is essentially a built-in version of the FLIP technique animation nerds have been hand-rolling for years. Except now it works for anything, including stuff that would normally be impossible to animate (like an element morphing into a completely different element).
The minimum-viable example
function toggleTheme() {
// 1. Wrap your DOM mutation in startViewTransition.
if (!document.startViewTransition) {
// Fallback for old browsers
document.body.classList.toggle("dark");
return;
}
document.startViewTransition(() => {
document.body.classList.toggle("dark");
});
}That is it. The browser will smoothly cross-fade the entire page between light and dark. No JS animation library. No CSS keyframes you wrote yourself.
Named transitions for shared elements
Give two elements (one on the old page, one on the new) the same view-transition-name, and the browser will animate from one's size and position to the other's. This is the "Apple Music album art zoom" pattern, available with zero JavaScript animation code.
/* Old page: the small thumbnail */
.card-thumb {
view-transition-name: hero-image;
}
/* New page: the big detail hero */
.detail-hero {
view-transition-name: hero-image;
}
/* Optional: customize the morph itself */
::view-transition-old(hero-image),
::view-transition-new(hero-image) {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}Each named element gets two pseudo-elements during the transition: ::view-transition-old(name) and ::view-transition-new(name). You style or animate those with regular CSS. The browser handles the position interpolation for you.
view-transition-name on the same page or the transition will skip. If you have a grid of cards, only one card at a time can have the shared name (typically the one you clicked).Cross-document transitions
Same-document transitions are great for SPAs. But the web is built on multi-page navigation, and the API now extends there too. Two CSS rules opt you in:
/* Opt both pages into cross-document transitions. */
@view-transition {
navigation: auto;
}
/* Shared element on both pages. */
.product-image {
view-transition-name: product-hero;
/* important: each instance must have a unique name across the doc */
}With this in place, clicking a link to another page on the same origin will trigger a real page navigation that the browser animates. No SPA framework required.
Speculation Rules: pre-render the next page
Cross-document transitions look magical, but if the next page takes 2 seconds to download, the animation either stalls or feels disconnected from the navigation. Speculation Rules let you tell the browser to pre-render likely next pages so they are ready the instant the user clicks.
<script type="speculationrules">
{
"prerender": [
{
"where": { "href_matches": "/products/*" },
"eagerness": "moderate"
}
]
}
</script>Combined with cross-document view transitions, the result feels instant. Click a product card and you are already on the detail page, with the hero image morphing into place.
Try it: list-to-detail morph
This sandbox shows a same-document version of the pattern: click a card, it animates into the detail view. Click back, it animates back. All three cards share a thumbnail; only the active one gets the shared name, so we set it dynamically.
if (document.startViewTransition) before calling it. Older browsers will just run your callback synchronously and the page works fine, just without animation.Quick quiz
What does document.startViewTransition(callback) actually do?
Recap
document.startViewTransition(cb)snapshots the page, runs your DOM mutation, and animates between states.- Matching
view-transition-nameon old and new elements triggers a shared-element morph. - Style the
::view-transition-old(name)and::view-transition-new(name)pseudo-elements to customize timing and effects. - Cross-document mode is enabled with
@view-transition { navigation: auto; }. - Pair cross-document transitions with Speculation Rules for instant navigation feel.