View Transitions cross-document, la fluidité d'une SPA sur un site multipage
Les View Transitions cross-document sont dix fois plus utilisées que les transitions des applications monopages. Quatre lignes de CSS suffisent à les activer. L'article construit une transition complète, puis pose la limite qui compte en production.

0,114 % des pages chargées dans Chrome activent une transition animée entre deux documents. Les transitions à l'intérieur d'un même document, celles que déclenchent les applications monopages, tournent à 0,011 % (opens in a new tab). Dix fois moins. La mesure (opens in a new tab) prend à revers l'intuition. La version réputée la plus difficile, animer le passage d'un document à un autre, est celle qui s'installe.
Ces transitions ont un nom, les View Transitions cross-document. Chrome les livre depuis la version 126 (opens in a new tab) en juin 2024, Safari depuis la 18.2 (opens in a new tab) en décembre 2024. Elles animent le passage d'une page à l'autre sur un site multipage ordinaire, celui qui sert du HTML et laisse le navigateur naviguer. Aucun routeur JavaScript, aucun état à préserver, aucun framework. L'activation tient en quatre lignes de CSS.
Le sujet dépasse la syntaxe. Cet article commence par ce que le navigateur détruit réellement entre deux pages, le vide qui a justifié le déplacement du routage côté client. Il construit ensuite une transition complète, du fondu par défaut aux éléments qui persistent d'une page à l'autre, puis aux animations qui changent selon le sens de navigation. Il se termine par la limite qui compte en production. Cette transition n'accélère aucun chargement. Il faut savoir avec quoi la compléter.
Le vide entre deux documents
Une navigation classique détruit un document et en construit un autre. Le navigateur atténue depuis longtemps la brutalité de l'opération en conservant l'ancienne page à l'écran jusqu'à ce que la nouvelle ait de quoi peindre. Le résultat n'est donc pas un écran blanc, c'est une bascule sèche. Un état visuel remplace l'autre sans rien entre les deux.
Ce que perd le visiteur à cet instant tient en une question. Quel élément est devenu quel élément ? La vignette cliquée dans une grille devient l'image d'en-tête de la page suivante, le titre de la carte devient le titre de l'article. Rien ne le montre. Le lien de continuité, le cerveau doit le reconstruire à chaque navigation. C'est un coût d'attention, du même ordre que ceux décrits à propos de la charge mentale d'une interface (opens in a new tab).
L'application monopage, single-page application en anglais et SPA en abrégé, a résolu ce problème en ne quittant jamais le document. Le routage passe en JavaScript, les changements de page deviennent des changements d'état et tout devient animable, puisque rien n'est détruit. La méthode fonctionne. Elle déplace aussi une part du travail du navigateur vers le code applicatif, la restauration du défilement, la gestion du focus, l'annonce des changements de page aux lecteurs d'écran, le traitement des erreurs réseau et la synchronisation de l'état (opens in a new tab). Pour une application, ce contrat se justifie. Pour un site vitrine, un catalogue, un blog ou une documentation, il se payait surtout pour l'animation.
Activer les View Transitions en quatre lignes de CSS
L'activation se déclare dans la feuille de style, avec une règle identique sur la page de départ et sur la page d'arrivée.
@view-transition {
navigation: auto;
}Activer les transitions entre documents
La règle doit figurer dans les deux documents. Si une seule des deux pages la déclare, rien ne se produit, puisque le navigateur a besoin de l'accord de celle qu'il quitte comme de celle qu'il ouvre. C'est la première cause de transition qui ne se déclenche pas. Elle se manifeste surtout quand une section du site charge une feuille de style différente du reste.
Le déclenchement obéit ensuite à des conditions strictes, détaillées dans la documentation MDN (opens in a new tab) de la règle @view-transition.
| Condition | Ce qui l'empêche |
|---|---|
| Les deux documents partagent le schéma, l'hôte et le port | Une redirection vers une autre origine dans la chaîne, même intermédiaire |
La règle @view-transition est présente des deux côtés | Une page servie sans la feuille de style commune |
Navigation de type push ou replace déclenchée depuis le contenu de la page | Une URL tapée dans la barre d'adresse ou ouverte depuis un signet |
Navigation de type traverse | Rien, les boutons Précédent et Suivant du navigateur sont acceptés |
Les conditions à réunir pour qu'une transition entre documents se déclenche
Ces conditions réunies, la transition par défaut est un fondu croisé sur l'intégralité de la page. Le navigateur capture l'ancien document juste avant qu'il disparaisse, capture le nouveau dès qu'il est prêt à peindre, puis fait passer l'un dans l'autre. Rien d'autre à écrire. Sur un site dont les pages partagent un en-tête et une navigation, le gain est déjà net, l'en-tête cesse de sursauter à chaque clic.
L'arbre de pseudo-éléments que le navigateur construit
Pour animer, le navigateur pose sur le nouveau document un arbre de pseudo-éléments qui contient les deux captures.
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)L'arbre de transition par défaut
::view-transition-old(root) est l'image de l'ancienne page, ::view-transition-new(root) celle de la nouvelle. Le groupe qui les contient porte l'animation de position et de taille, la paire gère le passage de l'une à l'autre. Par défaut cet arbre ne compte qu'un seul groupe nommé root, ce qui explique le comportement observé. La page entière est traitée comme un bloc unique, elle se fond donc d'un seul tenant.
Nommer ce qui doit persister d'une page à l'autre
Le vrai travail commence quand un élément doit survivre à la navigation au lieu de disparaître avec le reste. La propriété view-transition-name le sort du groupe root et lui donne le sien.
/* Page liste */
.card-cover {
view-transition-name: cover;
}
/* Page article */
.article-hero {
view-transition-name: cover;
}Un même nom dans les deux documents
Le nom sert de point de rendez-vous. Le navigateur trouve cover dans l'ancien document puis dans le nouveau, en déduit qu'il s'agit du même objet et anime le passage d'une position et d'une taille à l'autre. La vignette de la grille glisse et s'agrandit jusqu'à devenir l'image d'en-tête. Aucune coordonnée n'est calculée à la main, le navigateur mesure les deux états et interpole entre eux.
Deux contraintes gouvernent ce mécanisme. La première explique la majorité des transitions qui échouent en silence.
Un nom doit être unique dans le document au moment de la capture. Une grille de douze cartes qui déclarent toutes view-transition-name: cover produit douze éléments homonymes. Le navigateur abandonne alors la transition entière. Sur une page liste, la règle ne peut donc pas être statique. Elle doit désigner une carte précise, celle que le visiteur vient de cliquer.
L'événement pageswap, documenté sur MDN (opens in a new tab), permet de le faire au dernier moment, juste avant que le document de départ cède la place.
window.addEventListener("pageswap", async (event) => {
if (!event.viewTransition) return;
const target = new URL(event.activation.entry.url);
const link = document.querySelector(`a[href="${target.pathname}"]`);
if (!link) return;
const cover = link.querySelector(".card-cover");
cover.style.viewTransitionName = "cover";
await event.viewTransition.finished;
cover.style.viewTransitionName = "none";
});Nommer la carte cliquée juste avant la navigation
event.activation.entry.url donne l'URL de destination, ce qui permet de retrouver le lien correspondant dans la page et de nommer son image. L'attente de event.viewTransition.finished avant de retirer le nom laisse au navigateur le temps de prendre sa capture. L'événement symétrique pagereveal, lui aussi documenté sur MDN (opens in a new tab), joue le même rôle côté page d'arrivée, utile quand c'est le retour en arrière qui doit désigner une cible parmi plusieurs.
La seconde contrainte est propre aux transitions entre documents. La valeur match-element, qui demande au navigateur de générer un nom unique par élément, ne fonctionne que dans un même document. Les identifiants qu'elle produit ne traversent pas la frontière entre deux documents (opens in a new tab). Le raccourci qui simplifie tant les transitions à l'intérieur d'une application monopage est donc indisponible ici. Entre deux pages, les noms sont explicites ou il n'y a pas de correspondance.
Écrire ses propres animations
Les pseudo-éléments s'animent comme n'importe quel élément, avec animation et des @keyframes.
::view-transition-old(root) {
animation: 120ms ease-out both fade-out;
}
::view-transition-new(root) {
animation: 200ms ease-in both fade-in;
}
::view-transition-group(cover) {
animation-duration: 260ms;
animation-timing-function: ease-in-out;
}Remplacer le fondu par défaut
Le groupe porte le déplacement et le redimensionnement, old et new portent la disparition et l'apparition. Séparer les deux permet de faire sortir l'ancienne page vite puis entrer la nouvelle un peu plus lentement, l'asymétrie qui donne une impression de direction.
Pour appliquer un même traitement à plusieurs éléments nommés, view-transition-class évite de répéter les règles.
.card-cover,
.article-hero {
view-transition-class: media;
}
::view-transition-group(.media) {
animation-duration: 260ms;
}Grouper plusieurs transitions sous une classe
Le sélecteur universel (opens in a new tab) reste disponible pour toucher tous les groupes d'un coup avec ::view-transition-group(*). Sa spécificité est nulle, ce qui en fait une bonne base à surcharger ensuite nom par nom.
Un point de méthode évite des heures perdues. Ces règles doivent exister dans les deux documents, celui qui part comme celui qui arrive, puisque chacun fournit une moitié de la transition. Une feuille de style globale chargée sur tout le site règle la question sans y penser.
Distinguer l'aller du retour
Un même mouvement dans les deux sens paraît faux. La page suivante doit entrer par la droite, la page précédente revenir par la gauche. Le descripteur types déclare cette information et le sélecteur :active-view-transition-type() la lit sur l'élément racine, comme le détaille sa page MDN (opens in a new tab).
@view-transition {
navigation: auto;
types: forwards;
}
html:active-view-transition-type(forwards) {
&::view-transition-old(root) {
animation-name: slide-out-to-left;
}
&::view-transition-new(root) {
animation-name: slide-in-from-right;
}
}
html:active-view-transition-type(backwards) {
&::view-transition-old(root) {
animation-name: slide-out-to-right;
}
&::view-transition-new(root) {
animation-name: slide-in-from-left;
}
}Des animations différentes selon le sens
Un type déclaré dans le CSS reste fixe, ce qui suffit rarement. Le sens réel d'une navigation dépend de l'historique, pas de la page. L'événement pagereveal permet de l'ajouter au moment où la transition démarre, avant que les animations se résolvent.
window.addEventListener("pagereveal", (event) => {
if (!event.viewTransition) return;
const { from, entry, navigationType } = navigation.activation;
if (navigationType !== "traverse" || !from) {
event.viewTransition.types.add("forwards");
return;
}
const backwards = entry.index < from.index;
event.viewTransition.types.add(backwards ? "backwards" : "forwards");
});Déduire le sens depuis l'historique de navigation
L'index de l'entrée d'historique dit tout. Une destination dont l'index (opens in a new tab) est inférieur à celui de la page quittée signifie un retour en arrière. Le type ajouté avant le démarrage de la transition sélectionne alors les bonnes règles CSS. Le visiteur retrouve le sens de lecture attendu quand il utilise le bouton Précédent.
Cette transition ne rend aucune page plus rapide
Le point mérite d'être posé net. Une transition entre documents ne réduit aucun temps de chargement. Le navigateur ne capture le nouveau document qu'au moment où celui-ci est prêt à peindre, soit après la requête, la réponse et le rendu. Sur une page qui met huit cents millisecondes à répondre, la transition démarre après ces huit cents millisecondes et l'animation s'ajoute au total.
Ce que gagne le visiteur est de nature différente, la continuité. Le changement devient lisible, le lien entre les deux états visible. La sensation de fluidité d'une application monopage vient en réalité de deux choses distinctes, l'animation et l'absence d'attente. Les View Transitions fournissent la première. La seconde se traite ailleurs, avec la Speculation Rules API (opens in a new tab), qui demande au navigateur de préparer la page suivante avant le clic.
<script type="speculationrules">
{
"prerender": [
{
"where": { "href_matches": "/blog/*" },
"eagerness": "moderate"
}
]
}
</script>Préparer la page suivante avant le clic
Avec la page suivante déjà rendue, la transition démarre immédiatement au clic et l'illusion devient complète. Le mécanisme est un sujet à part entière, avec ses propres arbitrages de bande passante et de charge serveur. Retenir pour l'instant que les deux techniques se complètent, l'une supprime l'attente et l'autre la rend lisible quand elle subsiste.
Une conséquence pratique en découle sur la durée. Une transition longue retarde le moment où le visiteur peut agir sur la nouvelle page. Le repère se trouve dans la feuille de style du navigateur, qui anime les groupes de transition sur un quart de seconde (opens in a new tab). Rester dans cet ordre de grandeur garde la transition du côté de la continuité, l'allonger la fait basculer du côté de l'attente.
Le mouvement n'est pas neutre
Un déplacement de page en plein écran fait partie des animations qui déclenchent des troubles vestibulaires, nausée et vertige chez les personnes concernées. Le sujet a été traité en détail à propos de l'accessibilité des interactions (opens in a new tab). La réponse tient dans une requête média. Elle consiste à réduire le mouvement plutôt qu'à tout supprimer.
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*) {
animation-duration: 0s;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 100ms;
}
}Réduire le mouvement sans supprimer le repère visuel
Les déplacements et les changements de taille disparaissent, un fondu court subsiste. Le visiteur garde l'indication qu'il vient de changer de page sans subir le mouvement qui la produit. Supprimer toute animation reste possible, c'est simplement moins bon, le repère de changement se perd avec elle.
Le support des navigateurs et la décision d'adopter
Le support n'est pas complet. Chrome et Edge depuis la version 126, Safari depuis la 18.2. Firefox ne l'a pas encore (opens in a new tab), ce qui maintient la fonctionnalité au statut Baseline "limited availability". Firefox a livré les transitions à l'intérieur d'un même document avec la version 144 (opens in a new tab) en octobre 2025, c'est la moitié entre documents qui manque. Les transitions entre documents figurent parmi les vingt domaines prioritaires d'Interop 2026 (opens in a new tab), l'accord annuel par lequel les éditeurs de navigateurs s'engagent sur une liste commune.
Le comportement en l'absence de support est ce qui rend l'adoption simple. Un moteur qui ne connaît pas @view-transition ignore la règle, comme il ignore toute règle at-rule inconnue. Le visiteur sous Firefox obtient la navigation habituelle, sans animation et sans erreur. Rien à charger, rien à détecter, aucun repli à écrire. C'est le cas le plus favorable de l'amélioration progressive (opens in a new tab), une fonctionnalité dont l'absence ne retire rien à ce qui existait avant.
Reste le JavaScript facultatif du nommage dynamique. Les événements pageswap et pagereveal n'existent pas non plus dans Firefox, ce qui rend le test if (!event.viewTransition) return; doublement utile. L'écouteur ne se déclenche pas. S'il se déclenchait sans transition active, la fonction s'arrêterait immédiatement.
La décision se résume ainsi.
- Sur un site vitrine, un blog, un catalogue ou une documentation, activer sans hésiter. Quatre lignes de CSS pour un effet immédiat, c'est le meilleur rapport entre l'effort et le résultat qu'offre le CSS moderne
- Sur une application monopage existante, rien ne change.
document.startViewTransition()reste l'outil adapté, Baseline depuis octobre 2025 - Sur un projet qui hésitait à passer en monopage pour la seule fluidité des transitions, l'argument tombe
- Sur un parc de navigateurs contraint, intranet ou secteur public, activer quand même. L'absence de support ne dégrade rien
- Avant d'investir dans le nommage élément par élément, vérifier que le fondu par défaut ne suffit pas déjà. Il suffit plus souvent qu'on ne le croit
Ce que le routeur client garde et ce qu'il perd
Le choix entre multipage et monopage s'est longtemps joué en partie sur l'apparence. Une navigation classique donnait une bascule sèche, un routeur JavaScript donnait une transition. Cet écart n'existe plus. Un site qui sert du HTML statique obtient la même continuité visuelle avec quatre lignes de CSS, sans dépendance à installer et sans code à maintenir.
Le routeur client garde ce qui le justifiait vraiment, l'état partagé entre les écrans, les interfaces qui ne se rechargent jamais, le temps réel. Il perd un argument, celui de la fluidité. Pour un site vitrine, un blog ou un catalogue, cet argument ne suffit plus à financer un framework. C'est une ligne de moins au budget d'un projet et une dépendance de moins à mettre à jour dans deux ans.