Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 11.4Intermediate ~18 min · shot 4 of 7

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.

0/7 · 0%

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.com and https://b.com are 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.

index.html and ride.htmladd to filehtml
<!-- 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:

terminal
$ node cross-doc-run.cjs
── expected output ──
60ms home: pagereveal viewTransition=no (first load: nothing to animate from)
159ms home: pageswap viewTransition=yes activation.from=/ entry=/ride
174ms ride: pagereveal viewTransition=yes
174ms ride: reveal ready
439ms ride: reveal finished
--- back button
1697ms ride: pageswap viewTransition=yes activation.from=/ride entry=/
1707ms home: pagereveal viewTransition=yes
1957ms home: reveal finished
--- link to another origin
3273ms ride: pagereveal viewTransition=no

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. 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. 2

    Turn the opt-in off on one page

    Delete @view-transition from ride.html only and click. Measured: the new page's pagereveal has no viewTransition and the page just appears.

Code & diagrams

Read the opt-in rule and the events in this browser

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.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

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
pageswap and pagereveal (non-live: needs two real pages)js
// 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).

terminal
$ Count pagereveal events that have a viewTransition (Chromium 153, Vite dev server)
── what you'll see ──
inline rule: 40 of 40 navigations animated
linked file: 29 to 33 of 40 animated (two runs); the console showed 'InvalidStateError: Transition was aborted because of invalid state. ViewTransition opt-in disabled'

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 pageswap set card.style.viewTransitionName = 'hero' (document.activeElement was the clicked link, measured) and reset it in finished: the page may return from the back/forward cache with the name still set.

Remember this

  1. 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. 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-separated types: that the new page's CSS can match (measured: types: arriving matched there).

  3. 3

    Same origin, normal navigations. A link to another origin never animates (measured: the new page's pagereveal had no viewTransition). 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. 4

    Two events. pageswap fires on the old page just before it is replaced (event.viewTransition, and event.activation.entry.url for the destination). pagereveal fires on the new page before its first render. Measured locally: pageswap at 159 ms, pagereveal at 174 ms, finished 265 ms later. Each page sees only its own types: one added in the old page's pageswap did not reach the new page.

  5. 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. 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

01

What must be true for a normal link to produce a view transition, and what do pageswap and pagereveal do?

02

The transition sometimes does not play on a slow connection. What are the possible causes?

Practice

01

Make a product card's image morph into the detail page's image without naming every card.

02

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 pageswap and pagereveal run and what each is for

  • You can explain why a slow page cancels the transition and where the limit is