Cross-Document Page Morph
Deux vraies pages HTML et un lien ordinaire : la vignette de la page liste grandit jusqu'à devenir l'en-tête de la page article pendant que le navigateur change de page — @view-transition { navigation: auto; } et le même view-transition-name des deux côtés, zéro JavaScript, morph de 0,5 s à l'aller comme au bouton retour, navigation normale là où le navigateur ne sait pas faire.
Mis à jour le
Cet effet fait partie d'Effect.Labs — 840 effets vanilla, une partie gratuite, une partie premium. Transitions comprend 52 effets, dont 9 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
Le code tient en deux fichiers à poser côte à côte : page-liste.html (trois cartes, chacune un vrai lien <a href="page-article.html#natif">) et page-article.html (trois <article>). Les deux pages ouvrent leur feuille de style par la même règle, @view-transition { navigation: auto; } : c'est l'accord des deux côtés qui autorise le navigateur à animer le passage. Au clic, il photographie la page liste, charge la page article, cherche dans les deux les éléments qui portent le même view-transition-name — la vignette .xdvt-thumb--natif et l'en-tête .xdvt-hero--natif s'appellent tous deux xdvt-hero-natif — et fait glisser et grandir l'un vers l'autre en 0,5 s, courbe cubic-bezier(.2, .8, .2, 1) ; le reste de la page passe en fondu enchaîné. Aucune ligne de JavaScript : le champ JS est vide, tout est dans le CSS.
La vignette est large et basse (110 px de haut dans une carte d'au moins 200 px), l'en-tête plus haut (220 px sur toute la largeur) : sans précaution, chaque image garderait ses proportions et déborderait dessus et dessous pendant le morph. ::view-transition-old(*) et ::view-transition-new(*) reçoivent donc block-size: 100% et object-fit: cover, et ::view-transition-image-pair(*) un overflow: clip : chaque image remplit le cadre qui grandit, recadrée. Chaque carte a son propre nom (xdvt-hero-natif, -zero, -mpa), car un nom doit être unique dans une page : deux éléments visibles avec le même nom, et Chromium annule tout le morph (mesuré, message « Unexpected duplicate view-transition-name » dans la console). Sur la page article, les trois en-têtes portent les trois noms sans conflit, parce que seul l'article affiché est photographié ; les deux vignettes qui n'ont pas de partenaire s'effacent sur place pendant le fondu.
La page article porte les trois articles et l'ancre choisit lequel s'affiche : .xdvt-article est masqué, le premier s'affiche par défaut, :target affiche celui que vise l'adresse (#zero) et :has() masque alors le premier. C'est ce qui permet de livrer l'effet en deux fichiers seulement. Sur un vrai site, faites une page par article (article-natif.html…) : un seul <article>, plus de règles :target, chaque page a son titre et son adresse,. Le retour n'a pas besoin de code : le lien « ← Retour à la liste » comme le bouton retour du navigateur rejouent le morph dans l'autre sens, l'en-tête redevient vignette (mesuré dans Chromium 151 et WebKit 26.5). Les pages doivent être servies en HTTP ou HTTPS, depuis la même origine : un navigateur qui ne connaît pas ces transitions ignore la règle et charge la page comme d'habitude.
Accessibilité
- Mouvement réduit : les deux pages déclarent
@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } }. Mesuré sous émulation dans Chromium 151 et WebKit 26.5 : aucune animation, la page article remplace la liste d'un coup, à l'aller comme au retour. - Vraie navigation : chaque passage est un changement de page ordinaire. Le lecteur d'écran annonce le titre de la nouvelle page, le focus repart du haut du document, l'historique, le bouton retour, l'ouverture dans un nouvel onglet et le partage de l'adresse marchent sans rien gérer. La photo du morph est purement visuelle. Donnez à chaque page article un
<title>qui nomme l'article : la version à ancres en a un seul pour les trois (« Le carnet — article »), une raison de plus pour faire une page par article. - Clavier : les cartes sont de vrais liens, atteignables à la tabulation (dans Safari, Option + Tab ou le réglage « Tab parcourt tous les éléments »), avec un contour blanc de 2 px au focus (
:focus-visible). Entrée sur une carte joue le même morph qu'un clic (mesuré dans Chromium et WebKit). - Contraste : le texte secondaire des cartes (
#a3a9c9sur#161b36) est à 7,29:1, le surtitre (#8a90b0sur#0d1124) à 5,96:1, le texte de l'article à 11,9:1. Les dégradés des vignettes ne portent pas de texte ; si vous y écrivez, du blanc descend à 3,09:1 sur le bleu#6d8bffet à 1,25:1 sur le vert menthe#5cffd8: posez le texte sur un bandeau sombre.
Compatibilité navigateur
CSS seul : @view-transition, view-transition-name, les pseudo-éléments ::view-transition-*, :target et :has() ; aucun JavaScript, aucune dépendance. Mesuré avec Playwright, pages servies en HTTP depuis la même origine : morph à l'aller, au lien retour et au bouton retour du navigateur dans Chromium 151 et WebKit 26.5 (aussi dans une iframe de même origine) ; Firefox 153 change de page normalement, sans animation ni erreur, bien qu'il connaisse déjà les transitions dans une même page. Pas de morph entre deux origines différentes (mesuré entre localhost et 127.0.0.1), ni dans une iframe sandbox sans allow-same-origin. En fichiers ouverts en double-clic (file://), WebKit ne joue pas le morph et Chromium l'a joué dans notre test : ne vous y fiez pas, testez avec un petit serveur local. Support public des transitions entre documents : Chrome et Edge 126, Safari 18.2.
Un navigateur qui ne connaît pas @view-transition ignore la règle : le lien charge la page article comme n'importe quel lien, avec le même contenu, la même adresse et le même historique. Même chose sous mouvement réduit (navigation: none), entre deux origines différentes, ou si deux éléments visibles d'une page portent le même nom. Rien ne casse, seul le morph manque.
Le code
Structure HTML à coller dans votre page (le CSS + JS sont inclus avec Premium) :
<!-- page-liste.html : @view-transition { navigation: auto; } dans le 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 cartes : #natif, #zero, #mpa)
</div>
</main>
</body>
<!-- page-article.html : même règle, mêmes noms sur les en-têtes -->
<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>
Le code complet HTML + CSS + JS de Cross-Document Page Morph, prêt à coller — et les 840 effets. Ou 299 € TTC une fois, accès à vie.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Durée et courbe (CSS — ::view-transition-group(*)) |
0,5 s, cubic-bezier(.2, .8, .2, 1) | 0,3 s donne un passage vif, au-delà de 0,8 s on attend la page. Le * vise tous les groupes, fondu de la page compris ; pour régler un seul élément, nommez-le : ::view-transition-group(xdvt-hero-natif). |
Éléments partagés (CSS — view-transition-name) |
un nom par carte : xdvt-hero-natif, -zero, -mpa | Même nom sur la vignette et sur l'en-tête de l'article qu'elle ouvre, unique dans chaque page. Pour faire aussi glisser le titre, donnez un second nom (xdvt-title-natif) au libellé de la carte et au <h1> de l'article. |
Recadrage pendant le morph (CSS — ::view-transition-old(*), ::view-transition-new(*)) |
block-size: 100%, object-fit: cover, image-pair en overflow: clip | Sans ces règles, chaque image garde ses proportions et déborde du cadre quand la vignette et l'en-tête n'ont pas la même forme. object-fit: contain montre l'image entière, avec des bandes vides. |
Fondu du reste de la page (CSS — ::view-transition-old(root), ::view-transition-new(root)) |
fondu enchaîné du navigateur, 0,5 s | animation: none sur les deux supprime le fondu : le reste de la page change d'un coup et seul l'élément partagé bouge. Gardez l'en-tête du site identique sur les deux pages : il paraît alors immobile. |
Mouvement réduit (CSS — @media (prefers-reduced-motion: reduce)) |
@view-transition { navigation: none; } | Navigation sans aucune animation. Pour un fondu très court à la place, gardez navigation: auto dans ce bloc et réglez ::view-transition-group(*) à animation-duration: .15s. |
Choix de l'article (CSS — :target et :has()) |
une page article qui porte trois articles, le premier par défaut | Sur un vrai site, une page par article : un seul <article>, les quatre règles .xdvt-article supprimées, le lien devient article-natif.html. Les noms ne changent pas. |
FAQ
file://, les pages n'ont pas d'origine fiable : WebKit ne joue pas le morph, Chromium l'a joué dans notre test, mais rien ne le garantit. Pour tester, lancez un petit serveur dans le dossier (python3 -m http.server, puis http://localhost:8000/page-liste.html) ou passez par celui de votre éditeur. Même règle en ligne : pas de morph entre www. et le domaine nu, ni dans une iframe sandbox sans allow-same-origin.document.startViewTransition() et 2,5 Ko de JavaScript : utile dans une application d'une seule page, et elle joue aussi dans Firefox. Cross-Document Page Morph anime entre deux vraies pages, avec deux règles CSS et aucun script : chaque page garde son adresse, son historique et son référencement, mais Firefox (153 mesuré) se contente pour l'instant d'une navigation normale. Choisissez celui-ci pour un site fait de pages, l'autre pour une interface qui ne recharge pas.view-transition-name et sait animer dans une même page ; il ignore seulement @view-transition entre deux documents, sans erreur. Le jour où il l'applique, les mêmes deux fichiers morpheront chez lui comme dans Chrome et Safari.