Le démarrage d'une transition de vue avec document.startViewTransition() associe la superposition ::view-transition à l'élément :root, ce qui bloque les événements de pointeur sur le reste de la page, empêche les transitions simultanées et masque les en-têtes position: fixed ou les éléments [popover] ouverts (même lorsque des groupes de transition de vue imbriqués sont utilisés pour empêcher le débordement).
L'appel de element.startViewTransition() sur une racine de composant résout ce problème en limitant la transition à ce sous-arbre DOM : l'arbre de pseudo-éléments ::view-transition est injecté directement sur l'élément, ce qui préserve les contextes d'empilement externes et l'interactivité de la page tout en permettant à plusieurs composants d'effectuer une transition simultanément.
Implémentation
1. Déclencher une transition limitée
Appelez element.startViewTransition() sur l'élément conteneur racine du composant au lieu de document.startViewTransition(). L'élément sur lequel vous appelez startViewTransition() devient le champ d'application de la transition (l'élément de champ d'application) : seuls les éléments de son sous-arbre sont capturés, seul l'affichage du champ d'application s'arrête pendant le rappel de mise à jour, et la superposition ::view-transition obtenue correspond à la taille, à la forme et au contexte d'empilement de l'élément de champ d'application.
2. Nommer les éléments participants
Attribuez un view-transition-name aux éléments enfants du champ d'application qui doivent être animés. Vous pouvez utiliser view-transition-name: match-element sur les éléments de liste dynamiques pour dériver automatiquement le nom de transition de chaque élément à partir de l'identité de son nœud DOM, ou attribuer des noms explicites (qui doivent uniquement être uniques dans ce champ d'application).
Les noms sont isolés dans chaque champ d'application, ce qui évite les collisions entre les composants de différentes parties du DOM. Toutefois, les view-transition-name peuvent entrer en conflit avec les noms dans les transitions de vue à portée imbriquée. Pour éviter cela, ajoutez view-transition-scope: all à l'élément de champ d'application enfant.
3. Auto-participation et coupure de dépassement
L'élément de champ d'application participe automatiquement à sa propre transition de vue, car la feuille de style de l'user-agent lui applique view-transition-name: root et view-transition-group: contain. Ainsi, lorsque la géométrie de la portée est affectée par la modification que vous apportez au DOM, elle anime de manière fluide sa propre géométrie ainsi que ses enfants, dans le cadre de la transition de vue.
Lorsque le champ d'application limite son dépassement (overflow: hidden, scroll ou clip), ::view-transition-group-children(root) applique automatiquement overflow: clip, ce qui empêche les enfants en transition de dépasser les limites du composant.
4. Transitions simultanées et imbriquées
Des sous-arborescences de composants distinctes peuvent exécuter element.startViewTransition() simultanément sans s'ignorer mutuellement. Les champs d'application actifs reçoivent automatiquement view-transition-scope: all dans la feuille de style UA afin que les valeurs view-transition-name internes ne soient pas divulguées dans les transitions externes. Appliquez view-transition-scope: all en CSS sur les conteneurs de composants répétés pour empêcher les transitions externes de capturer les noms internes, même lorsque le composant est inactif.
Si une transition externe doit déplacer un composant alors que sa transition interne est active, attribuez un view-transition-name à un élément encapsulant le champ d'application interne. La transition externe capture et anime uniquement le wrapper au lieu de l'élément de champ d'application interne, ce qui permet à la transition interne de s'exécuter jusqu'à la fin.
Exemple
Aucun CSS n'est nécessaire sur l'élément de champ d'application lui-même (.card ou .card ul) pour établir le champ d'application. Le champ d'application est déterminé en appelant startViewTransition() sur l'élément au lieu de document (si le composant est imbriqué dans une transition de vue externe, consultez Exécuter une transition externe pour view-transition-scope: all).
/* 1. Name participating child items */
.card ul li {
view-transition-name: match-element;
}
/* 2. OPTIONAL: Customize transition timing (300ms is an example duration) */
::view-transition-group(*) {
animation-duration: 300ms;
animation-timing-function: ease-in-out;
}
/* 3. MANDATORY: Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
// 1. Trigger a transition scoped to a single component subtree
function runScopedTransition(scopeEl, updateDOM) {
// MANDATORY: Call startViewTransition on the component element, not document
const transition = scopeEl.startViewTransition(() => {
updateDOM();
});
// Re-triggering startViewTransition() on the same element skips the active transition
// and rejects `ready`. Catch the rejection to avoid unhandled promise rejection errors.
transition.ready.catch(() => {});
}
// 2. Reorder items by moving existing DOM nodes (required for `match-element`)
// Scope to `listEl` since only the list order changes, keeping controls outside `listEl` interactive
function reorderItems(listEl, orderedIds) {
const existing = new Map(
Array.from(listEl.children, (li) => [li.dataset.id, li]),
);
runScopedTransition(listEl, () => {
for (const id of orderedIds) {
const li = existing.get(id);
if (li) listEl.appendChild(li);
}
});
}
// 3. Remove an item while preserving exit animation and keyboard focus
// Scope to the outer `cardEl` so the card itself also transitions its height change
function removeItem(cardEl, itemEl) {
const hadFocus = itemEl.contains(document.activeElement);
const nextFocusTarget =
itemEl.nextElementSibling?.querySelector('button') ??
itemEl.previousElementSibling?.querySelector('button') ??
cardEl.querySelector('button');
runScopedTransition(cardEl, () => {
itemEl.remove();
if (hadFocus) {
nextFocusTarget?.focus();
}
});
}
Exécuter une transition externe
Lorsque vous démarrez une transition de vue en dehors du champ d'application prévu (par exemple, à partir d'un composant parent ou du document pour une transition au niveau de la page), la transition externe capture et anime l'élément du champ d'application et tout ce qu'il contient.
Pour éviter cela, encapsulez l'élément de champ d'application dans un élément wrapper distinct avec son propre view-transition-name, puis appliquez view-transition-scope: all à l'élément de champ d'application interne. Cela permet de capturer et d'animer le wrapper lui-même par la transition externe, tout en empêchant la capture du contenu du champ d'application interne.
<div class="page" id="page">
<div class="card-wrapper">
<section class="card" id="left">
<h2>Card A</h2>
<ul id="left-list">
<li>…</li>
</ul>
</section>
</div>
<div class="card-wrapper">
<section class="card" id="right">
<h2>Card B</h2>
<ul id="right-list">
<li>…</li>
</ul>
</section>
</div>
</div>
document.querySelector('#page').startViewTransition(() => {
// do your DOM updates here...
});
/* Apply a view-transition-name on the wrapper */
.card-wrapper {
view-transition-name: match-element;
}
/* Prevent the contents of the card being captured by an outer transition */
.card {
view-transition-scope: all;
}
.card ul li {
view-transition-name: match-element;
}
Vous pouvez également autoriser la transition externe à capturer les enfants. Pour conserver les effets de rognage, utilisez des groupes de transition de vue imbriqués pour rogner les enfants capturés aux limites de leur wrapper. Dans ce cas, vous devez toutefois appliquer manuellement le clip au pseudo ::view-transition-group-children() obtenu.
.card-wrapper {
overflow: clip;
view-transition-name: match-element; /* Capture me as part of the outer VT */
view-transition-group: contain; /* Nest children that also have a view-transition-name */
view-transition-class: card; /* For targeting purposes */
}
/* Manually copy back the clip onto the ::view-transition-group-children pseudo */
::view-transition-group-children(.card) {
overflow: clip;
}
.card ul li {
view-transition-name: match-element;
}
Contraintes et accessibilité
- OBLIGATOIRE : Appelez
startViewTransition()sur l'élément racine du composant plutôt quedocumentafin que les élémentsposition: fixedexternes et les superpositions[popover]restent superposés et que le reste de la page reste interactif. - OBLIGATOIRE : Conservez et déplacez les nœuds DOM existants (plutôt que de les recréer avec
innerHTML) lorsque vous utilisezview-transition-name: match-element, carmatch-elementsuit l'identité des nœuds DOM. - OBLIGATOIRE : Respectez les préférences de l'utilisateur concernant la réduction des animations à l'aide de
@media (prefers-reduced-motion: reduce)en définissantanimation: none !importantsur::view-transition-group(*),::view-transition-old(*)et::view-transition-new(*). - OBLIGATOIRE : Conservez le focus clavier lorsque vous supprimez un élément sélectionné pendant une transition en déplaçant le focus vers un élément ou une commande adjacents dans le rappel de mise à jour.
- À FAIRE : Utilisez des groupes de transitions de vue imbriqués (
view-transition-group: containetoverflow: clipsur::view-transition-group-children()) lorsqu'un élément capturé dans le champ d'application limite son propre dépassement et contient des éléments capturés imbriqués. - À FAIRE : Associez
transition.ready.catch(() => {})lorsque des interactions rapides peuvent déclencher à nouveauelement.startViewTransition()sur le même élément de champ d'application. - À FAIRE : Encapsulez un élément de champ d'application interne dans un élément wrapper nommé distinct si une transition externe doit animer la position du composant pendant qu'une transition interne est en cours d'exécution.
- À ÉVITER : Définir
view-transition-name: nonesur un élément de champ d'application qui limite son dépassement (overflow: hidden,scrollouclip) ou modifie sa propre géométrie, car la désactivation de l'auto-participation désactive la limitation automatique du dépassement.
Stratégies de remplacement
Si votre cible Baseline n'est pas compatible avec les transitions de vue limitées à un élément, effectuez une dégradation progressive en exécutant immédiatement le rappel de mise à jour du DOM sans animation. Bien que vous puissiez utiliser document.startViewTransition(), cela n'est pas recommandé pour les mises à jour de composants isolés, car une transition au niveau du document bloque les transitions de composants simultanées et masque les éléments position: fixed et [popover] dans la couche supérieure.
// Feature detect specifically on Element.prototype (not document.startViewTransition)
const supportsScopedTransition = 'startViewTransition' in Element.prototype;
function runWithTransition(scopeEl, updateDOM) {
if (!supportsScopedTransition) {
// Fallback: apply the DOM update immediately without animation
updateDOM();
return;
}
const transition = scopeEl.startViewTransition(() => {
updateDOM();
});
transition.ready.catch(() => {});
}
Si vous avez vraiment besoin d'une solution de remplacement à l'aide de document.startViewTransition(), vous devez vous assurer que les effets de découpage des conteneurs sont conservés, comme indiqué dans la section Exécuter une transition externe. Notez que cette approche de remplacement empêche l'exécution simultanée de plusieurs transitions de vue et masque les éléments position: fixed et [popover] dans la couche supérieure.