Shots in this scene
Cross-Document Transitions
In one line
Add @view-transition { navigation: auto; } to two same-origin pages and the browser animates the navigation between them, with named elements morphing across. pageswap and pagereveal let you adjust or skip it, and a slow new page cancels it.
Think of it like this
Two rooms joined by a doorway. If both leave the door open you watch a ball roll from one room into the other; if one keeps its door shut, the ball disappears and reappears. Each page must opt in, and both must be in the same building (same origin).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- @view-transition
- A CSS at-rule saying this page agrees to animate navigations. Both pages need it.
- same origin
- The same scheme, host and port.
https://a.comandhttps://b.comare different origins. - pageswap / pagereveal
- Events on the old page just before it is replaced, and on the new page before its first render.
- render-blocking
- A resource (such as a head stylesheet) the browser loads before showing the page.
Step by step
01Both pages opt in and name the element
Put the rule in the head of the launch page and the ride page, and view-transition-name: volt on the drawing in each. No script.
<!-- in the <head> of index.html AND ride.html -->
<style>
@view-transition { navigation: auto; }
.volt { view-transition-name: volt; }
</style>02Click the link and watch the events
We served two real pages from a tiny Node server and drove them with Playwright (Chromium 153), logging both events:
03Know the slow-page limit
We delayed a render-blocking stylesheet in the new page: 3.0 s and 3.8 s still animated (finished 270 and 266 ms later), 4.5 s and 6.0 s did not; with rel=expect, 1.5 s animated and 6 s did not.
04Adjust it in script when you must
The code block below shows pageswap and pagereveal handlers. Use pageswap to name or un-name elements on the old page, pagereveal for the new page, and either to call skipTransition().
Try it yourself
- 1
Run the real pair yourself
Build this phase's project, run
npm run dev, open/and click See the ride: the drawing flies. Press Back: it flies back. Record it in DevTools' Animations panel. - 2
Turn the opt-in off on one page
Delete
@view-transitionfromride.htmlonly and click. Measured: the new page'spagerevealhas noviewTransitionand the page just appears.
Code & diagrams
This page is one document, so nothing animates here. The example parses the at-rule through the CSS object model and checks the events. An unknown `navigation` value keeps the rule with an empty string.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
pageswap event: true
pagereveal event: true
ViewTransition type: function
CSSViewTransitionRule navigation=auto types=
CSSViewTransitionRule navigation=none types=
CSSViewTransitionRule navigation= types=
CSSViewTransitionRule navigation=auto types=fade slide
document.activeViewTransition: null// on the OLD page: choose what travels
addEventListener("pageswap", (event) => {
if (!event.viewTransition) return; // unsupported, or nothing to animate
const to = new URL(event.activation.entry.url).pathname;
if (to !== "/ride.html") event.viewTransition.skipTransition(); // only animate to the ride page
});
// on the NEW page: runs before the first render. An inline classic <script> in the <head> is sure to be
// in time; a module that still has imports to fetch may not be.
addEventListener("pagereveal", (event) => {
if (!event.viewTransition) return;
event.viewTransition.ready.catch(() => {}); // skipped: not an error
});
// hold the first render until the hero exists, so the morph has something to land on
// <link rel="expect" href="#hero" blocking="render">Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Put the opt-in in a linked stylesheet on the dev server
Remove the inline <style> and put the rule only in components.css. Open the launch page and click the link, 40 times (a fresh page each time).
Myth vs fact
Myth
A cross-document transition needs a single-page app or a framework.
Fact
It needs two ordinary same-origin pages and a few lines of CSS on each.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
On a list page, name only the clicked card: in
pageswapsetcard.style.viewTransitionName = 'hero'(document.activeElementwas the clicked link, measured) and reset it infinished: the page may return from the back/forward cache with the name still set.
Remember this
- 1
The problem this solves: a transition between pages used to need a single-page app. A plain link on a multi-page or server-rendered site reloaded with a flash of white. Now a normal link can animate.
- 2
The opt-in. Both pages contain
@view-transition { navigation: auto; }. It is read when the new page is revealed, so keep it where it arrives early: on the Vite dev server, with the rule in a linked stylesheet 33 of 40 navigations animated in one run and 29 of 40 in another; inline in a<style>in the head, 40 of 40 both times (the linked number varies with timing). The Lumen build keeps it inline. The rule also takes space-separatedtypes:that the new page's CSS can match (measured:types: arrivingmatched there). - 3
Same origin, normal navigations. A link to another origin never animates (measured: the new page's
pagerevealhad noviewTransition). A link click,location.href = …, back and forward all animated (about 250 ms); a reload did not. Both pages must opt in: with only one, nothing animated. - 4
Two events.
pageswapfires on the old page just before it is replaced (event.viewTransition, andevent.activation.entry.urlfor the destination).pagerevealfires on the new page before its first render. Measured locally:pageswapat 159 ms,pagerevealat 174 ms,finished265 ms later. Each page sees only its own types: one added in the old page'spageswapdid not reach the new page. - 5
A slow new page cancels it. The old page stays visible while the new one loads, so a slow response only means waiting. But render-blocking content after the response holds the reveal: a stylesheet delayed 3.0 s and 3.8 s still got the transition; at 4.5 s and 6 s it did not. So the limit is about 4 seconds.
<link rel="expect" href="#hero" blocking="render">holds the first render until that element exists: it ran at 1.5 s and was dropped at 6 s. - 6
Support. About 88.81% of browsers: Chrome 126, Safari 18.2, Firefox partly (caniuse, 2026-10-19). Elsewhere the link just navigates, so there is nothing to feature-detect.
Explain it without notes
What must be true for a normal link to produce a view transition, and what do pageswap and pagereveal do?
The transition sometimes does not play on a slow connection. What are the possible causes?
Practice
Make a product card's image morph into the detail page's image without naming every card.
Write a pagereveal handler that skips the transition when the visitor chose reduced motion on this site (localStorage key lumen-motion).
Done when you can
You can list what two pages need for a cross-document transition
You can say when
pageswapandpagerevealrun and what each is forYou can explain why a slow page cancels the transition and where the limit is