Éviter les problèmes de coupure (et plus encore) dans les transitions de vue en utilisant des groupes de transitions de vue imbriqués

Publié le 22 septembre 2025

Lorsque vous démarrez une transition de vue, le navigateur prend automatiquement des instantanés avant et après des éléments tagués avec un view-transition-name. Ces instantanés sont affichés dans une arborescence de pseudo-éléments. Par défaut, l'arborescence générée est "plate". Cela signifie que la hiérarchie d'origine dans le DOM est perdue et que tous les groupes de transition de vue capturés sont des frères et sœurs sous un seul pseudo-élément ::view-transition.

Cette approche d'arborescence à plat est suffisante pour de nombreux cas d'utilisation, mais certains cas d'utilisation de style ne peuvent pas être réalisés avec elle. Voici quelques exemples d'effets pouvant avoir un impact visuel inattendu dans un arbre plat :

  • Découpage (overflow, clip-path, border-radius) : le découpage affecte les enfants de l'élément, ce qui signifie que les éléments frères du groupe de transition de vue ne peuvent pas se découper mutuellement.
  • opacity, mask-image et filter : de même, ces effets sont conçus pour fonctionner sur une image entièrement rasterisée d'un arbre, en affectant les enfants plutôt que chaque élément individuellement.
  • Transformations 3D (transform-style, transform, perspective) : pour afficher toute la gamme d'animations de transformation 3D, une certaine hiérarchie doit être conservée.

L'exemple suivant montre un pseudo-arbre plat, avec des éléments qui sont coupés par un ancêtre dans l'arborescence DOM. Ces éléments perdent leur écrêtage lors de la transition de vue, ce qui entraîne un effet visuel cassé.

Enregistrement d'un effet de découpage cassé lors de l'exécution d'une transition de vue. Le texte devrait être coupé par la boîte de dialogue, mais il ne l'est pas. Le temps d'animation est ralenti pour exagérer l'effet.

Les groupes de transitions de vues imbriqués sont une extension des transitions de vues qui vous permet d'imbriquer des pseudo-éléments ::view-transition-group les uns dans les autres. Lorsque les groupes de transition de vue sont imbriqués, il est possible de restaurer des effets tels que le clipping pendant la transition.

Browser Support

  • Chrome: 140.
  • Edge: not supported.
  • Firefox: not supported.
  • Safari: not supported.

D'un pseudo-arbre plat à un pseudo-arbre imbriqué

Dans la démonstration suivante, vous pouvez cliquer sur l'avatar d'une personne pour afficher plus d'informations à son sujet. Les animations sont gérées par une transition de vue dans le même document, qui transforme le bouton sur lequel l'utilisateur a cliqué en boîte de dialogue, déplace l'avatar et le nom sur l'écran, et fait glisser les paragraphes de la boîte de dialogue vers le haut ou vers le bas.

Démo

Enregistrement de la démonstration

Enregistrement de la démonstration (ralenti)

Si vous examinez attentivement la démo, vous remarquerez un problème de transition : même si les paragraphes contenant la description sont des enfants de l'élément <dialog> dans le DOM, le texte n'est pas coupé par la boîte <dialog> lors de la transition :

<dialog id="info_bramus" closedby="any">
  <h2><img alt="…" class="avatar" height="96" width="96" src="avatar_bramus.jpg"> <span>Bramus</span></h2>
  <p>Bramus is …</p>
  <p>…</p>
</dialog>

L'application de overflow: clip sur <dialog> ne fait rien non plus.

Le problème réside dans la façon dont les transitions d'affichage créent et affichent leur pseudo-arborescence :

  • Dans le pseudo-arbre, par défaut, tous les instantanés sont frères et sœurs.
  • Le pseudo-arbre est affiché dans un pseudo-élément ::view-transition qui s'affiche au-dessus de l'ensemble du document.

Pour cette démo en particulier, l'arborescence DOM se présente comme suit :

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  └─ ::view-transition-image-pair(card)
  │  │     ├─ ::view-transition-old(card)
  │  │     └─ ::view-transition-new(card)
  │  ├─ ::view-transition-group(name)
  │  │  └─ ::view-transition-image-pair(name)
  │  │     ├─ ::view-transition-old(name)
  │  │     └─ ::view-transition-new(name)
  │  ├─ ::view-transition-group(avatar)
  │  │  └─ ::view-transition-image-pair(avatar)
  │  │     ├─ ::view-transition-old(avatar)
  │  │     └─ ::view-transition-new(avatar)
  │  ├─ ::view-transition-group(paragraph1.text)
  │  │  └─ ::view-transition-image-pair(paragraph1.text)
  │  │     └─ ::view-transition-new(paragraph1.text)
  │  └─ ::view-transition-group(paragraph2.text)
  │     └─ ::view-transition-image-pair(paragraph2.text)
  │        └─ ::view-transition-new(paragraph2.text)
  ├─ head
  └─ body
        └─ …

Comme les pseudos ::view-transition-group(.text) sont des frères et sœurs successifs du pseudo ::view-transition-group(card), ils sont peints au-dessus de la carte.

Pour que l'extrait ::view-transition-group(card) ::view-transition-group(.text) fonctionne, les pseudos ::view-transition-group(.text) doivent être des enfants de ::view-transition-group(card). Pour ce faire, utilisez view-transition-group, qui vous permet d'attribuer un "groupe parent" à un pseudo-élément ::view-transition-group() généré.

Pour modifier le groupe parent, vous avez deux options :

  • Sur le parent, définissez view-transition-group sur contain pour qu'il contienne tous les enfants avec un view-transition-name.
  • Sur tous les enfants, définissez view-transition-group sur le view-transition-name du parent. Vous pouvez également utiliser nearest pour cibler le groupe ancêtre le plus proche.

Pour cette démo, le code devient donc :

button.clicked,
dialog {
  view-transition-group: contain;
}

Ou

button.clicked,
dialog *,
  view-transition-group: nearest;
}

Avec ce code en place, les pseudos ::view-transition-group(.text) sont désormais imbriqués dans le pseudo ::view-transition-group(card). Pour ce faire, un pseudo-élément ::view-transition-group-children(…) supplémentaire est utilisé, ce qui permet de regrouper tous les pseudo-éléments imbriqués :

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  ├─ ::view-transition-image-pair(card)
  │  │  │  ├─ ::view-transition-old(card)
  │  │  │  └─ ::view-transition-new(card)
  │  │  └─::view-transition-group-children(card)
  │  │    ├─ ::view-transition-group(paragraph1.text)
  │  │    │  └─ ::view-transition-image-pair(paragraph1.text)
  │  │    │     └─ ::view-transition-new(paragraph1.text)
  │  │    └─ ::view-transition-group(paragraph2.text)
  │  │       └─ ::view-transition-image-pair(paragraph2.text)
  │  │          └─ ::view-transition-new(paragraph2.text)
  │  ├─ ::view-transition-group(name)
  │  │  └─ ::view-transition-image-pair(name)
  │  │     ├─ ::view-transition-old(name)
  │  │     └─ ::view-transition-new(name)
  │  └─ ::view-transition-group(avatar)
  │     └─ ::view-transition-image-pair(avatar)
  │        ├─ ::view-transition-old(avatar)
  │        └─ ::view-transition-new(avatar)
  ├─ head
  └─ body
        └─ …

Enfin, pour que le pseudo ::view-transition-group(card) écrête les paragraphes, appliquez overflow: clip au pseudo ::view-transition-group-children(card) :

::view-transition-group-children(card) {
  overflow: clip;
}

Le résultat est le suivant :

Démo

Enregistrement de la démonstration

Enregistrement de la démonstration (ralenti)

Le pseudo-élément ::view-transition-group-children n'est présent que lorsque des groupes imbriqués sont utilisés. Il est dimensionné à la border-box de l'élément d'origine et reçoit une bordure transparente de même forme et épaisseur que l'élément qui a généré le pseudo-élément, à savoir card dans l'exemple précédent.

Extraits et plus

Les groupes de transitions de vue imbriqués sont utilisés ailleurs que dans les effets de découpage. Les effets 3D en sont un autre exemple. Dans la démo suivante, vous pouvez faire pivoter la carte en 3D pendant la transition.

html:active-view-transition-type(open) {
    &::view-transition-old(card) {
        animation-name: rotate-out;
    }
    &::view-transition-new(card) {
        animation-name: rotate-in;
    }
}
html:active-view-transition-type(close) {
    &::view-transition-old(card) {
        animation-name: rotate-in;
    }
    &::view-transition-new(card) {
        animation-name: rotate-out;
    }
}

Sans groupes de transition de vue imbriqués, l'avatar et le nom ne pivotent pas avec la carte.

Démo

Enregistrement de la démonstration

Enregistrement de la démonstration (ralenti)

En imbriquant les pseudos de l'avatar et du nom dans la carte, l'effet 3D peut être restauré. Mais ce n'est pas tout. En plus de faire pivoter les pseudos ::view-transition-old(card) et ::view-transition-new(card), vous devez également faire pivoter celui de ::view-transition-group-children(card).

html:active-view-transition-type(open) {
    &::view-transition-group-children(card) {
        animation: rotate-in var(--duration) ease;
        backface-visibility: hidden;
    }
}
html:active-view-transition-type(close) {
    &::view-transition-group-children(card) {
        animation: rotate-out var(--duration) ease;
        backface-visibility: hidden;
    }
}

Démo

Enregistrement de la démonstration

Enregistrement de la démonstration (ralenti)

Plus de démos

Dans l'exemple suivant, des groupes de transitions de vue imbriqués sont utilisés pour s'assurer que les cartes sont coupées par leur élément de défilement parent. Vous pouvez activer ou désactiver l'utilisation de groupes de transition de vue imbriqués à l'aide des commandes incluses.

Démo

Enregistrement de la démonstration

L'intérêt de cette démo est que tous les pseudos ::view-transition-group(.card) sont imbriqués dans le pseudo ::view-transition-group(cards) ancêtre et sont coupés par celui-ci. L'élément #targeted-card est exclu, car son animation d'entrée/sortie ne doit pas être coupée par ::view-transition-group(cards).

/* The .cards wrapper contains all children */
.cards {
  view-transition-name: cards;
  view-transition-group: contain;
}

/* Contents that bleed out get clipped */
&::view-transition-group-children(cards) {
  overflow: clip;
}

/* Each card is given a v-t-name and v-t-class */
.card {
  view-transition-name: match-element;
  view-transition-class: card;
}

/* The targeted card is given a unique name (to style the pseudo differently)
   and shouldn't be contained by the ::view-transition-group-children(cards) pseudo */
#targeted-card {
  view-transition-name: targeted-card;
  view-transition-group: none;
}

Récapitulatif

Les transitions de vue imbriquées vous permettent de conserver une partie de la topologie de l'arborescence DOM lors de la construction des pseudo-éléments. Cela permet de créer des effets qui n'étaient pas possibles auparavant avec les transitions de vue, dont certains que nous avons décrits ici.

L'imbrication modifie le modèle de construction des transitions de vue et est destinée à créer des effets avancés. Comme indiqué, les transitions de vue à portée d'élément peuvent également produire un sous-ensemble d'effets avec un modèle plus simple. Nous vous encourageons à essayer les deux fonctionnalités pour déterminer celle qui répond le mieux à vos besoins.