Cross-Document View Transitions
Same-document view transitions animate a change made by script within one page. Cross-document view transitions do the same thing across an ordinary link click: the browser captures the old page, navigates, captures the new page, and animates between the two — for a multi-page site with no client-side router at all. For content sites, documentation and shops built as plain HTML, this is the feature that removes the last argument for turning a site into a single-page app purely for smooth navigation. This page covers opting in, shared elements across documents, customising the animation, and the conditions under which the browser quietly skips the transition. It belongs to View Transitions for CSS Developers in the CSS-Only Micro-Interactions & Animations guide.
How a cross-document transition happens
The mechanism mirrors the same-document version, with a navigation in the middle. When the user follows a same-origin link, the browser takes a snapshot of the outgoing page's named elements. It then loads the new page as usual. Once the new page is ready to render its first frame, the browser captures its named elements too, builds the familiar ::view-transition pseudo-element tree over the new page, and runs the animations. The old page is gone by then; only its snapshots remain.
The complete implementation
Two pages — a list of articles and an article — sharing one stylesheet. The shared stylesheet opts both documents in and names the elements that should morph.
<!-- list.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Articles</title>
<link rel="stylesheet" href="site.css">
</head>
<body>
<header class="site-header">Field Notes</header>
<main>
<a class="teaser" href="article.html">
<img class="teaser__img" src="lake.jpg" alt="" width="320" height="180"
style="view-transition-name: hero-lake">
<h2 style="view-transition-name: title-lake">A morning at the lake</h2>
</a>
</main>
</body>
</html>
<!-- article.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A morning at the lake</title>
<link rel="stylesheet" href="site.css">
</head>
<body>
<header class="site-header">Field Notes</header>
<main>
<img class="article__hero" src="lake.jpg" alt="Mist over a still lake at dawn"
width="1200" height="600" style="view-transition-name: hero-lake">
<h1 style="view-transition-name: title-lake">A morning at the lake</h1>
<p>Article text…</p>
</main>
</body>
</html>
/* site.css — loaded by both pages */
@view-transition {
navigation: auto;
}
/* The header is identical on both pages: keep it still. */
.site-header {
view-transition-name: site-header;
}
/* Shared elements get a slightly longer, eased morph. */
::view-transition-group(hero-lake),
::view-transition-group(title-lake) {
animation-duration: 350ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
/* The rest of the page crossfades quickly. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 200ms;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 1ms;
}
}
Clicking the teaser now morphs the thumbnail into the hero image and the heading into the page title, while the header stays fixed and everything else crossfades. The browser's back button plays the same transition in reverse, because the article page is now the old document and the list the new one.
The key technique: both sides opt in, names match across documents
Two rules make or break a cross-document transition. First, both documents must contain @view-transition { navigation: auto; }. The old page's opt-in tells the browser to capture before leaving; the new page's opt-in tells it to animate on arrival. A single shared stylesheet is the easiest way to guarantee both — a rule added to one template only is the most common reason nothing happens.
Second, a shared element is matched by its view-transition-name alone. The element can be a small thumbnail on one page and a full-width hero on the other, a different tag, in a different place in the DOM; if the names match, the browser animates the group from one box to the other. Names must still be unique within each document, so a listing page with many teasers needs a distinct name per item — typically generated from an ID in the template — and only the clicked item's name will find a partner on the detail page.
Customising per navigation with types
Often a transition should depend on where the user is going: slide left when moving forward through a series, slide right when going back. The @view-transition rule accepts a types descriptor, and those types can be matched with the :active-view-transition-type() selector:
@view-transition {
navigation: auto;
types: page-change;
}
html:active-view-transition-type(page-change) {
&::view-transition-old(root) { animation: slide-out 250ms ease-in both; }
&::view-transition-new(root) { animation: slide-in 250ms ease-out both; }
}
Choosing a type per navigation — forward versus back — requires the pageswap and pagereveal events, which let script inspect the navigation and add types to the transition. That keeps the default path CSS-only and moves only the decision into script. Without any script, every opted-in navigation gets the same type, which is often enough: a site with one consistent page-change animation feels coherent, and direction-aware motion can be added later without touching the CSS already written. View Transition Types and Classes covers the type system in detail.
When the browser skips the transition
Cross-document transitions fail silently by design, since the navigation must succeed either way. The transition is skipped when:
- The navigation is cross-origin. Even a subdomain change counts. Both URLs must share scheme, host and port.
- Either page did not opt in. Check that the rule is not inside a media query or layer that fails to apply on one of the pages.
- The new page is slow to render. Browsers impose a timeout between leaving the old page and the new page's first render; beyond it, the transition is abandoned. Large render-blocking resources on the destination page are the usual culprit. Keeping critical CSS small helps.
- The navigation type is excluded. Reloads and navigations the user triggers from the address bar do not animate; link clicks, form submissions to the same origin and history traversal do.
To keep the page usable while waiting, the browser holds the old page's frame on screen until the new page is ready, which means a slow destination feels frozen rather than blank. That is another reason to keep the first render of every page fast.
Preparing pages for smooth transitions
Because the new page must render before the animation starts, a cross-document transition exposes every slow first render on a site. A few habits keep transitions reliable:
- Keep render-blocking CSS small and shared. A single cached stylesheet across pages means the destination usually renders from cache almost immediately. Page-specific stylesheets that must download before first paint lengthen the frozen moment between click and animation.
- Give images dimensions. Shared elements are captured at the new page's first render. An image without
widthandheightattributes has no size yet, so the morph targets a zero-height box and then jumps. Explicit dimensions, oraspect-ratioin CSS, give the capture the right geometry. - Keep names stable. A name applied by a script after load does not exist at capture time. Put
view-transition-namein the markup or stylesheet so it is present on the first frame. - Avoid naming huge elements. Each named element becomes a separate snapshot image. Naming a long article body means capturing a very large bitmap on both pages; name the hero, title and navigation, and let the root crossfade handle the rest.
When a transition behaves oddly, the Animations panel in Chromium's DevTools can pause and slow the transition so each pseudo-element can be inspected, just as with same-document transitions.
Browser support
The cross-document @view-transition rule is supported in Chrome and Edge 126+ and Safari 18.2+. It is not supported in Firefox's stable releases, where navigation happens instantly as it always has. Same-document transitions and view-transition-name are wider: Chrome and Edge 111+, Firefox 144+ and Safari 18+. Because unsupporting browsers simply ignore the at-rule, no feature detection is needed. The prefers-reduced-motion media feature is supported in Chrome 74+, Edge 79+, Firefox 63+ and Safari 10.1+.
FAQ
Do cross-document view transitions need JavaScript?
No. Adding @view-transition { navigation: auto; } to the CSS of both the outgoing and incoming page is enough. The browser captures the old page, loads the new one, and crossfades between them. JavaScript is only needed for advanced cases such as choosing a transition type per link.
Why is my cross-document transition not running? The most common causes are that only one page opted in, the navigation is cross-origin, the navigation was a reload or started from the address bar, or the new page took too long to render its first frame. Both pages must be same-origin and both must include the rule.
Can shared elements morph between two pages?
Yes. Give the element the same view-transition-name on both pages, such as a product thumbnail on the listing and the hero image on the detail page. The browser animates the group's position and size between the two captures.
What happens in browsers without support? The rule is ignored and navigation works exactly as before, with an instant page swap. Cross-document transitions are a pure enhancement; there is nothing to polyfill or feature-test.
Related
- View Transitions for CSS Developers — the parent guide.
- Same-Document View Transitions — the single-page counterpart.
- view-transition-name and Shared Elements — naming rules in depth.
- View Transitions and Reduced Motion — substituting gentler motion.
Related articles
More pages in the same section.