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.

Capture, navigate, capture, animate Four steps left to right. Step one: the user clicks a link and the old page is captured as snapshots. Step two: the browser navigates and loads the new document. Step three: when the new page is ready to render, its named elements are captured. Step four: the pseudo-element tree animates from the old snapshots to the new live page. A link click with a transition in the middle 1. Old page captured on link click 2. Navigate new document loads 3. New page captured at first render 4. Animate old snapshots to live page Both documents must include @view-transition { navigation: auto; } Same origin only. No script required.

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.

Names pair elements across two documents On the left, a list page with three teaser thumbnails; one is named hero-lake. On the right, an article page with a large hero image also named hero-lake. An arrow shows the group morphing from the thumbnail's box to the hero's box. The other two teasers have names with no partner, so they simply fade out. Matching names morph; unmatched names fade list.html hero-lake hero-pier hero-ridge article.html hero-lake pier and ridge: no partner, fade

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.

Four checks before the transition plays A vertical chain of four checks. Is the navigation same-origin? Did both pages opt in with the at-rule? Is it a link click, form submission or history traversal rather than a reload or address-bar entry? Did the new page render its first frame in time? A no at any step leads to an instant page swap on the right. Four yeses lead to the transition at the bottom. Any "no" means an instant swap Same origin? Both pages opted in? Link, form or back/forward? New page rendered in time? Transition plays Instant swap no error, no event no

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 width and height attributes has no size yet, so the morph targets a zero-height box and then jumps. Explicit dimensions, or aspect-ratio in 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-name in 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 articles

More pages in the same section.