Published: Sep 22, 2025
When you start a view transition, the browser automatically takes before and after snapshots of elements tagged with a view-transition-name
. These snapshots get rendered in a tree of pseudo-elements. By default, the generated tree is "flat". This means that the original hierarchy in the DOM is lost, and all captured view transition groups are siblings under a single ::view-transition
pseudo-element.
This flat tree approach is sufficient for many use-cases, but there are some styling use-cases that cannot be achieved with it. The following are examples of effects that can have an unexpected visual effect in a flat tree:
- Clipping (
overflow
,clip-path
,border-radius
): clipping affects the children of the element, which means that view transition group siblings cannot clip each other. opacity
,mask-image
andfilter
: similarly, these effects are designed to work on a fully rasterized image of a tree, affecting the children, rather than affecting each item individually.- 3D transforms (
transform-style
,transform
,perspective
): to display the full range of 3D transform animations, some hierarchy needs to be maintained.
The following example shows a flat pseudo-tree, with elements that are clipped by an ancestor in the DOM tree. These elements lose their clipping during the view transition, resulting in a broken visual effect.
Nested view transition groups is an extension to view transitions that lets you nest ::view-transition-group
pseudo-elements within each other. When view transition groups are nested, it's possible to restore effects such as clipping during the transition.
Browser Support
From a flat pseudo-tree to a nested pseudo-tree
In the following demo you can click a person's avatar to see more info about that person. The animations are handled by a same-document view transition that morphs the clicked button into the dialog, moves the avatar and name across the screen, and slides the paragraphs from the dialog up or down.
Live Demo
Demo Recording
Demo Recording (Slowed down)
If you look closely at the demo you'll see that there's an issue with the transition: even though the paragraphs with the description are children of the <dialog>
element in the DOM, the text is not clipped by the <dialog>
s box during the 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>
Applying overflow: clip
on the <dialog>
also doesn't do anything.
The problem is the way view transitions build and render their pseudo tree:
- In the pseudo-tree, by default, all snapshots are siblings of each other.
- The pseudo-tree gets rendered in a
::view-transition
pseudo-element which renders on top of the entire document.
For this demo specifically, the DOM tree looks like the following:
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
└─ …
Because the ::view-transition-group(.text)
pseudos are succeeding siblings of the ::view-transition-group(card)
pseudo, they get painted on top of the card.
To have ::view-transition-group(card)
clip ::view-transition-group(.text)
, the ::view-transition-group(.text)
pseudos should be children of the ::view-transition-group(card)
. For this, use view-transition-group
which lets you assign a "parent group" for a generated ::view-transition-group()
pseudo-element.
To change the parent group you have two options:
- On the parent, set the
view-transition-group
tocontain
, to have it contain all children with aview-transition-name
. - On all children, set the
view-transition-group
to theview-transition-name
of the parent. You can also usenearest
to target the nearest ancestor group.
So for this demo, to use nested view transition groups, the code becomes:
button.clicked,
dialog {
view-transition-group: contain;
}
Or
button.clicked,
dialog *,
view-transition-group: nearest;
}
With this code in place, the ::view-transition-group(.text)
pseudos now get nested inside the ::view-transition-group(card)
pseudo. This is done in an extra ::view-transition-group-children(…)
pseudo, which keeps all nested pseudos together:
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
└─ …
Finally, to have the ::view-transition-group(card)
pseudo clip the paragraphs, apply overflow: clip
onto the ::view-transition-group-children(card)
pseudo:
::view-transition-group-children(card) {
overflow: clip;
}
The result is the following:
Live Demo
Demo Recording
Demo Recording (Slowed down)
The ::view-transition-group-children
pseudo is only present when nested groups are used. It is sized to the border-box of the original element and is given a transparent border with the same shape and border thickness as the element that generated the pseudo element—card
in the previous example.
Clipping and more
Nested view transitions groups are used in places other than clipping effects. Another example are 3D effects. In the following demo there is an option to rotate the card in 3D during the 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;
}
}
Without nested view transition groups, the avatar and name don't rotate along with the card.
Live Demo
Demo Recording
Demo Recording (Slowed down)
By nesting the avatar and name pseudos inside the card, the 3D effect can be restored. But that's not the only thing you need to do. In addition to rotating the ::view-transition-old(card)
and ::view-transition-new(card)
pseudos, you also need to rotate the ::view-transition-group-children(card)
one.
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;
}
}
Live Demo
Demo Recording
Demo Recording (Slowed down)
More demos
In the following example nested view transition groups are used to make sure the cards get clipped by their ancestor scroller. You can toggle the use of nested view transition groups on or off using the included controls.
Live Demo
Demo Recording
The interesting thing about this demo is that all ::view-transition-group(.card)
pseudos get nested inside—and clipped by—the ancestor ::view-transition-group(cards)
pseudo. The #targeted-card
one is excluded because its entry/exit animation shouldn't be clipped by the ::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;
}
Recap
Nested view transitions let you preserve some of the topology of the DOM tree when constructing the pseudo elements. This unlocks a variety of effects not previously possible with view transitions, some of which we described here.
Nesting changes the model of how view transitions are constructed, and is meant to be used to create advanced effects. As noted, element-scoped view transitions can also accomplish a subset of the effects with a simpler model. We encourage you to try both features to decide which one best fits your needs.