Cross-Document Page Morph
Two real HTML pages and a plain link: the list page thumbnail grows into the article page header while the browser switches pages — @view-transition { navigation: auto; } and the same view-transition-name on both sides, zero JavaScript, a 0.5 s morph going forward and on the back button, a normal page load wherever the browser can't do it.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →
3 usage examples



How it works
The code is two files that sit side by side: page-liste.html (three cards, each a real <a href="page-article.html#natif"> link) and page-article.html (three <article> elements). Both pages open their stylesheet with the same rule, @view-transition { navigation: auto; }: it takes both sides opting in for the browser to animate the switch. On click, it takes a snapshot of the list page, loads the article page, looks in both for elements with the same view-transition-name — the .xdvt-thumb--natif thumbnail and the .xdvt-hero--natif header are both named xdvt-hero-natif — and slides and grows one into the other in 0.5 s, with a cubic-bezier(.2, .8, .2, 1) curve; the rest of the page crossfades. Not a single line of JavaScript: the JS field is empty, everything lives in the CSS.
The thumbnail is wide and short (110 px tall in a card at least 200 px wide), the header taller (220 px across the full width): left alone, each image would keep its proportions and spill over the top and bottom during the morph. So ::view-transition-old(*) and ::view-transition-new(*) get block-size: 100% and object-fit: cover, and ::view-transition-image-pair(*) gets overflow: clip: each image fills the growing frame, cropped. Each card has its own name (xdvt-hero-natif, -zero, -mpa), because a name must be unique within a page: two visible elements with the same name and Chromium cancels the whole morph (measured, with an "Unexpected duplicate view-transition-name" message in the console). On the article page, the three headers carry the three names without conflict, because only the displayed article is captured; the two thumbnails with no partner fade out in place.
The article page holds all three articles and the anchor picks which one shows: .xdvt-article is hidden, the first one shows by default, :target shows the one the address points to (#zero) and :has() then hides the first. That is what lets the effect ship as only two files. On a real site, make one page per article (article-natif.html…): a single <article>, no more :target rules, each page gets its own title and address,. Going back needs no code: the "← Retour à la liste" link and the browser back button both replay the morph in reverse, the header turns back into the thumbnail (measured in Chromium 151 and WebKit 26.5). The pages must be served over HTTP or HTTPS, from the same origin: a browser that doesn't know these transitions ignores the rule and loads the page as usual. The demo text in both files is in French: replace it with your own content.
Accessibility
- Reduced motion: both pages declare
@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } }. Measured under emulation in Chromium 151 and WebKit 26.5: no animation, the article page replaces the list at once, going forward and back. - Real navigation: each switch is an ordinary page change. The screen reader announces the new page title, focus starts again at the top of the document, history, the back button, opening in a new tab and sharing the address all work with nothing to manage. The morph snapshot is purely visual. Give each article page a
<title>that names the article: the anchor version has a single one for all three ("Le carnet — article"), one more reason to make one page per article. - Keyboard: the cards are real links, reachable with Tab (in Safari, Option + Tab or the "Press Tab to highlight each item" setting), with a 2 px white outline on focus (
:focus-visible). Enter on a card plays the same morph as a click (measured in Chromium and WebKit). - Contrast: the cards' secondary text (
#a3a9c9on#161b36) is 7.29:1, the overline (#8a90b0on#0d1124) 5.96:1, the article text 11.9:1. The thumbnail gradients carry no text; if you write on them, white drops to 3.09:1 on the blue#6d8bffand to 1.25:1 on the mint#5cffd8: put the text on a dark band.
Browser compatibility
CSS only: @view-transition, view-transition-name, the ::view-transition-* pseudo-elements, :target and :has(); no JavaScript, no dependency. Measured with Playwright, pages served over HTTP from the same origin: morph going forward, on the back link and on the browser back button in Chromium 151 and WebKit 26.5 (also inside a same-origin iframe); Firefox 153 switches pages normally, with no animation and no error, even though it already supports transitions within a single page. No morph between two different origins (measured between localhost and 127.0.0.1), nor inside a sandbox iframe without allow-same-origin. With files opened by double-click (file://), WebKit doesn't play the morph and Chromium did in our test: don't rely on it, test with a small local server. Public support for cross-document transitions: Chrome and Edge 126, Safari 18.2.
A browser that doesn't know @view-transition ignores the rule: the link loads the article page like any other link, with the same content, the same address and the same history. Same thing under reduced motion (navigation: none), between two different origins, or if two visible elements on a page share a name. Nothing breaks, only the morph is missing.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<!-- page-liste.html: @view-transition { navigation: auto; } in the style -->
<body class="xdvt-page">
<main class="xdvt-wrap">
<p class="xdvt-h">…</p>
<h1 class="xdvt-title">…</h1>
<div class="xdvt-grid">
<a class="xdvt-card" href="page-article.html#natif">
<span class="xdvt-thumb xdvt-thumb--natif"></span>
<span class="lbl">…</span>
<span class="meta">…</span>
</a> … (3 cards: #natif, #zero, #mpa)
</div>
</main>
</body>
<!-- page-article.html: same rule, same names on the headers -->
<body class="xdvt-page">
<main class="xdvt-wrap">
<p class="xdvt-h">…</p>
<a class="xdvt-back" href="page-liste.html">← …</a>
<article class="xdvt-article" id="natif">
<div class="xdvt-hero xdvt-hero--natif"></div>
<h1>…</h1>
<p>…</p>
</article> … (3 articles: natif, zero, mpa)
</main>
</body>
The full HTML + CSS + JS of Cross-Document Page Morph, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Duration and curve (CSS — ::view-transition-group(*)) |
0.5 s, cubic-bezier(.2, .8, .2, 1) | 0.3 s gives a brisk switch, beyond 0.8 s visitors wait for the page. The * targets every group, page crossfade included; to tune a single element, name it: ::view-transition-group(xdvt-hero-natif). |
Shared elements (CSS — view-transition-name) |
one name per card: xdvt-hero-natif, -zero, -mpa | Same name on the thumbnail and on the header of the article it opens, unique within each page. To slide the title too, give a second name (xdvt-title-natif) to the card label and to the article's <h1>. |
Cropping during the morph (CSS — ::view-transition-old(*), ::view-transition-new(*)) |
block-size: 100%, object-fit: cover, image-pair with overflow: clip | Without these rules, each image keeps its proportions and spills out of the frame when the thumbnail and the header have different shapes. object-fit: contain shows the whole image, with empty bands. |
Rest of the page crossfade (CSS — ::view-transition-old(root), ::view-transition-new(root)) |
browser crossfade, 0.5 s | animation: none on both removes the crossfade: the rest of the page switches at once and only the shared element moves. Keep the site header identical on both pages: it then looks still. |
Reduced motion (CSS — @media (prefers-reduced-motion: reduce)) |
@view-transition { navigation: none; } | Navigation with no animation at all. For a very short fade instead, keep navigation: auto in this block and set ::view-transition-group(*) to animation-duration: .15s. |
Picking the article (CSS — :target and :has()) |
one article page holding three articles, the first by default | On a real site, one page per article: a single <article>, the four .xdvt-article rules removed, the link becomes article-natif.html. The names don't change. |
FAQ
file://, the pages have no reliable origin: WebKit doesn't play the morph, Chromium did in our test, but nothing guarantees it. To test, start a small server in the folder (python3 -m http.server, then http://localhost:8000/page-liste.html) or use your editor's. Same rule online: no morph between www. and the bare domain, nor inside a sandbox iframe without allow-same-origin.document.startViewTransition() and 2.5 KB of JavaScript: useful in a single-page app, and it also plays in Firefox. Cross-Document Page Morph animates between two real pages, with two CSS rules and no script: each page keeps its address, its history and its search ranking, but Firefox (153 measured) does a normal page load for now. Pick this one for a site made of pages, the other for an interface that doesn't reload.view-transition-name and can animate within a single page; it only ignores @view-transition between two documents, with no error. The day it applies it, the same two files will morph there as they do in Chrome and Safari.