![Bramus](https://web.dev/images/authors/bramus.jpg) Bramus [X](https://twitter.com/bramus) [GitHub](https://github.com/bramus) [Mastodon](https://front-end.social/@bramus) [Bluesky](https://bsky.app/profile/bram.us) [Homepage](https://www.bram.us/)


Published: Mar 27, 2026

<br />

Element-scoped view transitions let multiple view transitions run simultaneously, allow ongoing view transitions to be nested within another, and resolve `z-index` issues you might encounter with document-scoped view transitions---all while keeping the rest of the page interactive. Read this guide to learn how to use them.
[Video](https://www.youtube.com/watch?v=LaRA-zBubsE) Promotional video: Re-imagine the web with Scoped View Transitions. [Try a live demo](https://view-transitions.chrome.dev/my-patagonia-trip/) (Chrome 147 or later)

## The need for more narrowly scoped view transitions

When you start a [same-document view transition](https://developer.chrome.com/docs/web-platform/view-transitions/same-document.md.txt) with `document.startViewTransition()` (or through its [cross-document counterpart](https://developer.chrome.com/docs/web-platform/view-transitions/cross-document.md.txt)), the browser scopes the resulting view transition to the document.

After the update callback executes and the browser snapshots all necessary elements, the resulting [`::view-transition` overlay and its tree of pseudo-elements](https://developer.chrome.com/docs/web-platform/view-transitions/same-document.md.txt#how_these_transitions_work) attaches to the `:root` element, `html` in the following example.

    html
      ├─ ::view-transition
      │  └─ ::view-transition-group(root)
      │     └─ ::view-transition-image-pair(root)
      │        ├─ ::view-transition-old(root)
      │        └─ ::view-transition-new(root)
      ├─ head
      └─ body
         └─ ...

Because the `::view-transition` layer renders on top of the transition root, this can lead to unexpected situations. For example, elements that participate in a view transition might suddenly overlap other non-participants, or elements might no longer be clipped by their ancestor wrapper during the view transition.

### Live Demo

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/xbEpjEL?height=520&amp;default-tab=result" style="height: 520px; width: 100%; border: 0;" title="Document-Scoped View Transition"></iframe>

### Demo Recording

[Re-enabling `pointer-events` on `::view-transition`](https://www.bram.us/2025/01/29/view-transitions-page-interactivity/) or [using nested view transition groups](https://developer.chrome.com/docs/css-ui/view-transitions/nested-view-transition-groups.md.txt) can resolve some side effects that document-scoped view transitions introduce. However, these methods cannot solve all issues.

For example, elements with `position: fixed` or popovers are still obscured by a document-scoped view transition while the transition is active --- also known as [the `z-index` issue](https://www.nicchan.me/blog/view-transitions-and-stacking-context/).

Toggle the popover in the following demo, and then select the **Shuffle** button to start a document-scoped view transition. Nested view transition groups resolve the clipping issue, but the layering issue remains.

### Live Demo

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/emdyrBd?height=520&amp;default-tab=result" style="height: 520px; width: 100%; border: 0;" title="Document-Scoped View Transition with Nested View Transition Groups vs Popover"></iframe>

### Demo Recording

One workaround is to capture the `popover` as part of the view transition by giving it a `view-transition-name`. While this might work for a single instance, it is cumbersome to maintain and unnecessarily strains the snapshotting process.

## Element-scoped view transitions

Element-scoped view transitions let you start a view transition on a subtree of the DOM. Instead of calling `document.startViewTransition()`, you call `element.startViewTransition()` on an arbitrary element, which scopes the view transition to that element.

In the following snippet, the browser starts an element-scoped view transition on the `<ul>` element.

```javascript
document.querySelector('ul').startViewTransition({
  callback: () => {
    // ... code that manipulates the contents of <ul>
  },
})
```

The element where you invoke `element.startViewTransition()`---for example the `<ul>`---is called the transition root or the scope.

When the browser scopes a view transition to an element, it is isolated from the rest of the DOM:

- The browser looks for elements to snapshot only within the scope's subtree.
- During the snapshotting process---while the `update` callback executes---only the rendering of the scope halts.
- The resulting `::view-transition` pseudo-tree injects onto the transition root.

For example, with the `<ul>`, the DOM tree looks like this while the view transition is active:

    html
      ├─ head
      └─ body
         ├─ ul
         │  ├─ ::view-transition
         │  │  └─ ::view-transition-group(root)
         │  │     ├─ ::view-transition-group-children(root)
         │  │     │  └─ ...
         │  │     └─ ::view-transition-image-pair(root)
         │  │        ├─ ::view-transition-old(root)
         │  │        └─ ::view-transition-new(root)
         │  ├─ li
         │  ├─ li
         │  └─ li
         ├─ button#showpopover
         ├─ button#reorder
         └─ div#popover
            └─ p

The `::view-transition` pseudo has the same size and shape as the transition root and renders only on top of the transition root. Because of this, the layering order of elements outside of the transition root is respected.

For example, if you have a popover that is visible above the `<ul>` element and then start an element-scoped view transition on the `<ul>` element, the popover is not obscured by the view transition's pseudo-tree.

In the following demo, try it out. It has two buttons. The first button toggles the popover, and the second button reorders the list items using an element-scoped view transition.

### Live Demo

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/myrpLxo?height=520&amp;default-tab=result" style="height: 520px; width: 100%; border: 0;" title="Element-Scoped View Transition vs Popover"></iframe>

### Demo Recording

Because element-scoped view transitions are used, the popover remains visible on top of the `<ul>` element while the transition is active.

Furthermore, the elements outside the `<ul>` element---for example the buttons---remain interactive, because those elements are not part of the scope.

> [!NOTE]
> **Note:** Just as calling `document.startViewTransition()` a second time causes a document-scoped transition to skip the existing one, an element-scoped transition follows similar logic. If an element is already hosting a transition, calling `element.startViewTransition()` again skips the existing transition in favor of the new one.

## Self-participating scopes and nested view transition groups

When you start an element-scoped view transition on an element that clips its overflow (that is, when its `overflow` is set to `hidden`, `scroll`, or `clip`), you notice that the contents of the view transition remain visually clipped.

This is because element-scoped view transitions handle the following automatically:

- The scope automatically gets `view-transition-name: root` applied, which makes it self-participating.
- The scope automatically gets `view-transition-group: contain` applied to enable [nested view transition groups](https://developer.chrome.com/docs/css-ui/view-transitions/nested-view-transition-groups.md.txt).
- The resulting `::view-transition-group-children(root)` pseudo automatically clips its contents using `overflow: clip` if the scope root clips its overflow, which prevents the pseudos from visually bleeding out of the transition root.

As a result, you can keep the CSS you use with element-scoped view transitions focused only on the elements you want to capture. For example, in the list demo, the CSS only adds names to the list items:

```css
ul li {
  view-transition-name: match-element;
  view-transition-class: album;
}
```

> [!NOTE]
> **Note:** These automatic styles are set in the UA stylesheet. You can opt out of self-participation by setting `view-transition-name: none` in your styles. If you do so, your scope should probably not clip its contents or visually change as part of the view transition.

In the following demo, try it out. It lets you override self-participation. When the scope is self-participating (the default behavior), everything works as expected. When the scope is not self-participating, its border immediately changes, and its contents bleed out of the wrapper during the transition.

### Live Demo

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/jEMYxQr?height=590&amp;default-tab=result" style="height: 590px; width: 100%; border: 0;" title="Element-Scoped View Transitions: Self-Participation"></iframe>

### Demo Recording

For reference, the pseudo-tree for this demo with self-participation looks like this:

    html
      ├─ head
      └─ body
         ├─ ul
         │  ├─ ::view-transition
         │  │  └─ ::view-transition-group(root)
         │  │     ├─ ::view-transition-group-children(root)
         │  │     │  ├─ ::view-transition-group(item1)
         │  │     │  │  └─ ::view-transition-image-pair(item1)
         │  │     │  │     ├─ ::view-transition-old(item1)
         │  │     │  │     └─ ::view-transition-new(item1)
         │  │     │  ├─ ::view-transition-group(item2)
         │  │     │  │  └─ ...
         │  │     │  ...
         │  │     └─ ::view-transition-image-pair(root)
         │  │        ├─ ::view-transition-old(root)
         │  │        └─ ::view-transition-new(root)
         │  ├─ li
         │  ├─ li
         │  └─ li
         └─ button#reorder

Because the transition root, the `<ul>` element, vertically clips its contents, the `::view-transition-group-children(root)` also automatically applies a clip.

> [!NOTE]
> **Note:** When using [classic scrollbars](https://developer.chrome.com/docs/css-ui/scrollbar-styling.md.txt#classic_and_overlay_scrollbars), nested view transition groups overlay the scrollbars ([demo](https://codepen.io/web-dot-dev/pen/gbwozqV)). This is a known bug, tracked in [CrBug 475236700](https://issues.chromium.org/issues/475236700).

## Concurrent element-scoped view transitions

Because element-scoped view transitions run in isolation, multiple element-scoped view transitions can run simultaneously if they have a different scope.

The following demo has two reorder buttons, one for each list. Each button starts an element-scoped view transition only on its respective list. Because the DOM trees of both lists don't overlap, the two element-scoped view transitions can run simultaneously in isolation.

### Live Demo

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev-/embed/vEXpjPM?height=550&amp;default-tab=result" style="height: 550px; width: 100%; border: 0;" title="Element-Scoped View Transitions: Self-Participation"></iframe>

### Demo Recording

This isolated nature also lets you reuse `view-transition-name` values across different scopes. As long as a name remains unique within its scope, there is no conflict.

## Nested element-scoped view transitions and view-transition-name containment

When the DOM trees of multiple element-scoped view transitions overlap, there is a risk of `view-transition-name` value collision. For this reason, the browser automatically assigns `view-transition-scope: all` to active element-scoped view transitions to mitigate this risk.

Similar to how `anchor-scope` scopes `anchor-name` values, the `view-transition-scope` property ensures that `view-transition-name` values scope to the element's subtree. The property accepts `none`, a list of names you want to scope, or `all` to scope all values.

In addition to preventing names from bleeding out, `view-transition-scope` also prevents an element and its contents from capture by an outer, concurrent view transition. When the snapshotting process traverses the subtree to find an element to snapshot, it ignores elements (and their entire subtree) that have `view-transition-scope: all` applied. This assumes those elements are already participating in a different element-scoped view transition.

The following demo is a variation of the previous one. In addition to the two buttons that shuffle the list contents, it also has a **Swap** button to swap the lists. Toggling a `.reversed` class on the `#lists-wrapper` handles the swapping.

### Live Demo

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/ogzpdLE?height=520&amp;default-tab=result" style="height: 520px; width: 100%; border: 0;" title="Inner and Outer View Transitions, thanks to Element-Scoped View Transitions."></iframe>

### Demo Recording

Because `view-transition-scope: all` automatically applies during the shuffle transition, you can start a concurrent, outer swap transition while the shuffle transition is still ongoing.

Because `view-transition-scope: all` also prevents an element from being snapshotted in an outer transition, the demo also adds `view-transition-name` values to the elements wrapping the `<ul>` elements.

```css
#list1-wrapper, #list2-wrapper {
  view-transition-name: attr(id type(<custom-ident>));
}
```

> [!NOTE]
> **Note:** There is [an open issue at the CSS Working Group](https://github.com/w3c/csswg-drafts/issues/12322) that discusses whether self-participating scopes can participate in an outer transition. We might relax this restriction in the future.

The pseudo-tree for this demo, after starting a shuffle on the second list and then swapping both lists, looks like this:

    html
      ├─ head
      └─ body
         └─ #lists-wrapper.reversed (SCOPE)
            ├─ ::view-transition
            │  └─ ::view-transition-group(lists-wrapper)
            │     ├─ ::view-transition-group-children(lists-wrapper)
            │     │  ├─ ::view-transition-group(list1-wrapper)
            │     │  │  └─ ::view-transition-image-pair(list1-wrapper)
            │     │  │     ├─ ::view-transition-old(list1-wrapper)
            │     │  │     └─ ::view-transition-new(list1-wrapper)
            │     │  └─ ::view-transition-group(list2-wrapper)
            │     │     └─ ::view-transition-image-pair(list2-wrapper)
            │     │        ├─ ::view-transition-old(list2-wrapper)
            │     │        └─ ::view-transition-new(list2-wrapper)
            │     └─ ::view-transition-image-pair(lists-wrapper)
            │        ├─ ::view-transition-old(lists-wrapper)
            │        └─ ::view-transition-new(lists-wrapper)
            ├─ div#list1-wrapper
            │  ├─ ul
            │  │  ├─ li#item1
            │  │  ├─ li#item2
            │  │  └─ li#item3
            │  └─ button.reorder
            └─ div#list2-wrapper
               ├─ ul (SCOPE)
               │  ├─ ::view-transition
               │  │  └─ ::view-transition-group(list)
               │  │     ├─ ::view-transition-group-children(list    )
               │  │     │  ├─ ::view-transition-group(item4)
               │  │     │  │  └─ ::view-transition-image-pair(item4)
               │  │     │  │     ├─ ::view-transition-old(item4)
               │  │     │  │     └─ ::view-transition-new(item4)
               │  │     │  ├─ ::view-transition-group(item5)
               │  │     │  │  └─ ...
               │  │     │  ...
               │  │     └─ ::view-transition-image-pair(list)
               │  │        ├─ ::view-transition-old(list)
               │  │        └─ ::view-transition-new(list)
               │  ├─ li#item4
               │  ├─ li#item5
               │  └─ li#item6
               └─ button.reorder

> [!TIP]
> **Tip:** The `view-transition-scope` property is particularly useful for components that appear multiple times in a single document. Applying `view-transition-scope: all` to a component's wrapper prevents all transitions starting outside of the wrapper from capturing its internal elements.

> [!NOTE]
> **Note:** A tagged element cannot participate in more than one active transition simultaneously. If you try to start a transition inside an already running transition, or one that uses an element already participating in a different transition, the browser skips the newly started transition.

## Learn more

To learn more about element-scoped view transitions, see [the explainer](https://github.com/WICG/view-transitions/blob/main/scoped-transitions.md), [the css-view-transitions-2 specification](https://drafts.csswg.org/css-view-transitions-2/), and the [list of open spec edits](https://github.com/w3c/csswg-drafts/issues?q=is:issue+state:open+label:css-view-transitions-2+label:%22Needs+Edits%22).