Skip to content

Last updated: First published:

View Transition Names and Generated Pseudo-Elements

What are those pseudo-elements of the View Transition API and how can I style and animate them?

The key feature of the View Transition API is that it generates images for parts of your DOM and operates solely on those images. This enables the API to display sections of the old page exactly as they appeared before the transition began.

None of your actual DOM elements are directly visible during the transition. Everything is covered by the generated images, which are moved around in their own layer above your web page.

Think of it like a theater: A curtain drops, painted with the last scene, while the stage is being rebuilt behind it. On the curtain, images gradually shift to depict the next scene. Then, the curtain lifts to reveal the new stage. Just like in modern theater, there can be multiple curtains: some closer to the audience, others further back.

::view-transition
1
2
3
4
5
6
7
DOM
3
4
5
6
7

While theater can create magic, view transitions go even further with their “curtains.” These curtains not only move independently, but they can also change in size, orientation, opacity, and more. Some curtains even act as dynamic surfaces, projecting constantly changing content.

But that’s about it.

Now that we’ve covered the general concept, we can move on from the magic and dive into the technical details.

You tell the view transition API which elements should be animated by assigning view-transition-name CSS properties to the HTML elements.

Assigning a view transition name using inline styling
<main style="view-transition-name: main">
Assigning a view transition name with a stylesheet
main {
view-transition-name: main;
}

At the start of the view transition, the view transition names have to be unique in the DOM.

There are two exceptions:

  • The API ignores the view-transition-name of a fragmented element, i.e. an element that consists of two or more boxes, like an inline element that spans two lines.

  • The API ignores the view-transition-name of an element if that element is not rendered, like the <head>, or if it is skipped. For instance, you can use the same view-transition-name multiple times within the same DOM, provided that all but one instance have their display CSS property set to none. Looking for an example? This technique is utilized in the intra-document image morph examples on this site. This trick also works with an ancestor with content-visibility: hidden but not with a simple visibility: hidden directly on the element.

The static user agent stylesheet provides one default name for the document, called root.

static user agent stylesheet
:root {
view-transition-name: root;
}

This default defines an animation for the current viewport. If you want to animate only parts of the document, you can opt out of the global transition by overriding that default in your own styles by setting the value to none.

:root {
view-transition-name: none;
}

Do not quote the names. They are not strings but custom-ident values. Ensure proper escaping of special characters. The simplest, most portable approach is to just use upper and lower case letters from A to Z, digits from 0 to 9, an underscore _ or a hyphen - character. But don’t start the name with a digit. While all other characters with a code < 128 need to be escaped, codes > 127 like 😄 seem to work just fine in view transition names. In browsers, you can use CSS.escape() to properly escape strings as custom identifiers.

It is not important to have much imagination when choosing names. They can be the similar to the id of an element or name of a unique tag like body or footer.

The opposite problem to view transition names not being unique is that elements need to have any view-transition-name assigned in order to trigger a group or morph animation. Often, the actual value doesn’t matter. What’s important is simply having a value defined. For large numbers of elements, doing this manually can be cumbersome. You can automate it with JavaScript, as with the headings on this site or in this demo. However, it is also possible to do this with only CSS by using the auto value, no JavaScript needed.

.tile {
view-transition-name: auto;
}
.card {
view-transition-name: match-element;
}

Check if your current browser already supports it.

Both, auto and match-element, assign a generated name that is bound to the DOM element. Those names typically look like -ua-auto-..... Two different elements are always assigned different values. When you set the view transition name multiple times for the same element that way, it will always get the same name.

Safari, first checks for an existing id attribute, and instead copies its value if it exists. Chromium browsers don’t.

With cross-document view transitions, auto-generated names of the two documents will never be the same, not even when id attributes would match.

match-element will always assign a random name, no matter wether the id attribute is set or not. On Chromium browsers, auto and match-element seem to be the same thing.

On Chromium browsers you would use the following incantation to use the id attribute as an view transition name:

view-transition-name: attr(id type(<custom-ident>), none);

Here type(<custom-ident>) is necessary to make the id a custom-ident, and none is the default value to be used if there is no id attribute.

Fun fact: The following works in Chrome like the original Safari version of auto: Use the id attribute if defined, otherwise make up some name.

view-transition-name: attr(id type(<custom-ident>), auto);

I would not recommend auto for cross-document view transitions. There are alternatives that work cross-browser and also for cross-document navigation.

Of course view transition names do not need to be static. You can assign them conditionally, e.g. inside a media query or with a selector that checks for a special CSS class further up the DOM. Another interesting way for conditional assignment of view transition names is to let them depend on view transition types.

Because they are animatable CSS properties, view transition names can even be changed using other animations. So if you like you could automatically change a name over time or depending on the scroll position with scroll-driven animations.

The components that form the “curtain” are called view transition pseudo-elements. While they aren’t real DOM elements, they can be targeted with CSS selectors and styled using CSS rules. You may already be familiar with other HTML pseudo-elements like ::before or ::first-letter.

The View Transition API introduces five types of pseudo-elements:

  • ::view-transition
  • ::view-transition-group
  • ::view-transition-group-children
  • ::view-transition-image-pair
  • ::view-transition-old
  • ::view-transition-new

The ::view-transition pseudo-element, which is rooted at the document’s documentElement[^scoped], is the root of all pseudo-elements created by the View Transition API.

This will change when scoped view transitions are available, allowing you to root the pseudo-element tree on any element.

All these pseudo-elements only exist briefly during the transition effect, making them difficult to spot in the DOM.

Viewing pseudo-elements in the browser’s developer tools

To make spotting pseudo-elements in the DOM easier, you can use the animation panel of the browser’s Development Tools and press the pause button before you start the navigation. When you playback the animations, you can examine the DOM including the pseudo-elements.

Alternatively, in the Inspection Chamber’s Full Control Mode, time also freezes, allowing for detailed examination of the pseudo-elements. During an active view transition, click on a name in the Animation Groups panel to copy a command to your clipboard. You can then paste this command into the browser’s DevTools console for closer inspection.

Terminal window
inspect(top.document.querySelector("#vtbag-main-frame").contentDocument.querySelector(":root"))
<html lang=​"en">​
::view-transition // the parent of all groups
​::view-transition-group(root) // the group of the document's documentElement
​::view-transition-group(myName) // the group for "view-transition-name: myName"
...
​<head>​…​</head>
​<body>​…​</body>
​</html>​

Here root is the automatically assigned view-transition-name for the document’s documentElement (:root). myName is an arbitrary example view-transition-name.

Introducing pseudo-elements for parts of the DOM allows the View Transition API to display images of how the DOM looked even if it has changed or disappeared.

Each element in the old DOM with a view-transition-name: someName CSS property generates a ::view-transition-old(someName) pseudo-element with the same name. Similarly, for elements in the new DOM with a view-transition-name: someName CSS property, ::view-transition-new(someName) pseudo-elements are created with the corresponding name.

These pseudo-elements represent images of their original elements. For ::view-transition-old, think of it as a screenshot capturing how the element looked just before the view transition began.

The ::view-transition-new images are no simple screenshots but life replaced elements. While they behave similarly to screenshots in that they are bitmap images whose height and width can be adjusted, they aren’t static: If the underlying elements in the new DOM change, the pixels of the replaced element update as well.

This might be confusing, as the original element is usually hidden behind the ::view-transition pseudo-element (the “curtain”). Here’s the key difference: old images are immutable. For example, if you show the old image of a video player during a view transition, the image is frozen. New images, however, are live. If you display a new image of a video player, the video will continue to play during the transition.

If you capture an element with width w and height h, the pseudo-element will have the same height and width. But in addition it will also show the overflowing ink, like box-shadows, of the original element outside the w x h rectangle.

The View Transition API handles the image of the root <html> element in a special way. It covers only the viewport, not the entire page and no overflowing ink. If you shift this image up or down, it won’t reveal anything beyond the viewport’s boundaries. To capture the entire page, use the <body> element instead of the <html> element. However, keep in mind that browsers may consider such an image too large. They might capture only part of it, skip it, or even cancel the entire view transition. Chromium appears to implement partial capture for oversized images.

Because view transition names must be unique inside their DOM, there can be at most one old image and one new image with a given name. The API assumes that elements with the same view-transition-name on both the old and the new pages represent the same logical concept. It is not necessary for these elements to be implemented the same way or even use the same HTML tags. The old and new images form a pair, and this pair is the only child of the group. Both ::view-transition-group and ::view-transition-image-pair are named using the view-transition-name. All groups are direct children of the ::view-transition pseudo-element. Thus, a typical structure looks like this, where a pair can have one or two children.

::view-transition // the parent of all groups
​::view-transition-group(root) // the transition group of the :root element
::view-transition-image-pair(root) // the image pair for the root group
::view-transition-old(root) // with the old image of the root group
::view-transition-new(root) // and the new image of the root group
​::view-transition-group(name1) // the group for "view-transition-name: name1"
::view-transition-image-pair(name1) // with an image pair
::view-transition-old(name1) // that only has the old image
​::view-transition-group(name2) // the group for "view-transition-name: name2"
::view-transition-image-pair(name2) // with an image pair
::view-transition-new(name2) // that only has the new image

The term “pair” might be a bit misleading here: it refers to having at least one and at most two children, but there can be instances where only an old image or only a new image is present.

If both images are present, their order is a) old, b) new.

The image-pairs are the direct parents of the ::view-transition-old and ::view-transition-new images. They allow you to apply common styling to both images, set CSS isolation (default is isolate) for the pair, and to use :only-child to style exit/entry transition different from morph transitions. In a way that will continue to work once nested view transition groups are available.

With the one to one relation between groups and image pairs, the group looks a bit redundant. This 1:1 relationship will change in the future. Today the groups form a simple sequence and the order of the elements in this list also defines the standard paint order of the pseudos.

Nested view transition groups will add the possibility to arrange view transition groups in trees.

selector {
view-transition-name: my-name;
view-transition-group: other;
}

Here the definition of the view transition name creates a view transition group my-name. Adding the view-transition-group property makes the my-name group a child of the other group defined elsewhere. Reserved values are none, normal, contain, and nearest with their obvious ;-) meaning.

::view-transition // the parent of all groups
​::view-transition-group(other) // a top level group
::view-transition-image-pair(other)
::view-transition-old(other)
::view-transition-new(other)
::view-transition-group-children(other) // a container for all child groups
​::view-transition-group(my-name) // a group that is a child of the "other" group
::view-transition-image-pair(my-name)
::view-transition-old(my-name)

Nested view transition groups are especially interesting as they will allow clipping of child groups.

The ::view-transition-group-children pseudo-element is a relatively new addition to the View Transition API. Check whether your browser already supports it.

If supported, ::view-transition-group-children appears after the parent group’s image-pair (if any) and becomes the direct parent of all nested groups. This pseudo-element is only inserted for groups where the view-transition-group property is set. The name in parentheses for a group children pseudo-element matches that of its group.

In browsers that support nested group names but do not yet support ::view-transition-group-children, nested groups are direct children of the parent ::view-transition-group, and they may appear before the image-pair.

The ::view-transition-old pseudo elements are created in the paint-order of the elements of the old DOM. When an element with a view-transition-name is encountered, its old image is captured. During this process, other elements with view transition names are effectively ignored, as if their visibility were set to hidden. This ensures that the images have “holes” where other images can be properly placed.

The image of the :root element, if not opted out, covers the whole viewport. Its view transition group is the first in the list of ::view-transition-group children of the ::view-transition pseudo-element. Subsequent groups are added after the root group once their old images have been created.

Creation of old images Creation of old images

Pseudo-element tree after creation of the old images

For same-document view transitions, the creation of old images is the first step after calling startViewTransition(). For cross-document view transitions, creation of old images occurs right after the pageswap event is fired. The new images are processed when the updateCallbackDone promise resolves (for same-document view transitions) or right after the pagereveal event fires (for cross-document view transitions).

The process for creating new images is similar to the creation of old image pseudos. The key difference is that a ::view-transition-group for a specific view-transition-name may already exist when the ::view-transition-new image is created. In such cases, the new image is added to the existing ::view-transition-image-pair of that group. The image pair will then contain both the old and new images, always in that order.

If no existing group is found, a new ::view-transition-image-pair and ::view-transition-group pseudo-element is created for the view-transition-name and added to the end of the ::view-transition pseudo-element’s group list. This pair will then contain only the new image.

Addition of new images Addition of new images

Pseudo-element tree after creation of the new images

As with the old DOM, the pre-order depth-first traversal of the new DOM typically identifies the :root element as the first to have its image captured.

In a typical setup, where the View Transition API automatically assigns the root view transition name to the :root element in both the old and new DOM, the group for the root transition name will already exist when the image of the :root element is captured in the new DOM. Thus, the first five lines of the output, as shown above, are common in most settings.

When examining the ::view-transition-groups in order, the elements at the start of the list will have at least an old image, while those towards the end will have at least a new image.

Before the view transition starts, the pseudo elements are rendered on top of the current page.

The first pseudo-element that is rendered for a view transition is ::view-transition. It has the size of the viewport including scrollbars, and retractable UI parts on mobile. The spec refers to this as the snapshot containing block. The ::view-transition pseudo-element is fully transparent by default. It works like a glass pane that prevents the user from interacting with elements below it (as long as you don’t set pointer-events: none on it).

The transition groups are rendered in the order they were created, which is the same as the order in which they appear as children of the ::view-transition pseudo-element. Groups at the beginning of the list are rendered first, with later groups painted on top.

Within the image pair, the old image is painted first and the new image is painted on top of it.

There is a clear separation between regular DOM elements and the view transition pseudo-elements. The pseudo-elements form their own stacking context, known as the view transition layer. This one is special as it sits in front of all other elements of the page. No regular page elements can be forced into or above the view transition layer, not even with high z-index values.

One advantage of using images and a separate view transition layer is that animations are not restricted by the original DOM tree structure. However, this can lead to unexpected side effects if you apply view-transition-name attributes as an afterthought.

For example, if an element is partially hidden by other elements and you assign it a view transition name, it may unexpectedly appear in front them during the transition. To manage this, either treat it as a feature or avoid such cases. Alternatively, you can assign a view transition name to the obscuring elements. This ensures that they also define images in the view transition layer.

The View Transition API stacks the images in the original paint order. You can use the z-index CSS property to rearrange the view transition groups:

::view-transition-group(elevated-group) {
z-index: 1;
}

If you have prominent elements on your page that should always be in the foreground, even through view transitions, consider giving them an view transition name to lift them to the view transition layer and raise them there with an appropriate z-index.

The same approach can also be used to move the old image in front of the new image inside its image pair:

::view-transition-old(important-old) {
z-index: 1;
}

With cross-document transitions the browser has to guess what would be a good time to start the animaitons of the view transition. It needs to load at least the HTML with the elements that should participate in a view transition. And it definitively needs to load the stylesheets that describe the view transition. But it would be a bad user experience if it would wait for a long page to load completely and await all scripts to run before the transition starts.

Start of view transitions will always await the <head> to be loaded. External stylesheets inside the head will also be awaited for. If you want to make sure that view transitions wait for an external script, add the blocking="render" attribute to the script element. You can also instruct view transitions to wait until some fragment URL of the current page is loaded by adding a link to the <head> like this:

<link rel="expect" href="#somewhere" blocking="render"/>

If the fragment in the href attribute does not exist on the page, view transitions will wait for the entire page to load before they start. Just note that the browser may log an error in this case, and it might cancel the view transition entirely if the load takes too long.

Try pointing the href to an HTML element that ends just below the fold to avoid disrupting the incremental loading of the rest of your page.

I haven’t yet been able to wait for an image to load, but the View Transition API points toward a different approach. Example 5 suggests checking if an image has fully loaded, and if not, modifying the view transitions to handle the situation rather than waiting for the image to load.

Prefetching or preloading resources can significantly reduce wait times. On Chromium browsers, you can use speculation rules to handle preloading.