Last updated: First published:
Pseudo-Elements of the View Transition API
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.
The Theater Curtain
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.
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.
Types of Pseudo-Elements
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-image-pair
::view-transition-old
::view-transition-new
The ::view-transition
pseudo-element, which is rooted at the document’s documentElement, is the root of all pseudo-elements created by the View Transition API.
All these pseudo-elements only exist briefly during the transition effect, making them difficult to spot in the DOM.
Old and New Image Pseudos
Introducing images for parts of the DOM allows you to display 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 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. Replaced elements, however, are live. If you display a new image of a video player, the video will continue to play during the transition.
DOM elements without an explicit view-transition-name
won’t generate their own images, with one exception: the document’s :root
element. The View Transition API automatically assigns view-transition-name: root
to it. You can override this with your own name or opt out entirely by assigning the special value none
:
This removes the view transition name from the document’s :root
element.
Groups and Image-Pairs
Because view transition names must be unique1 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.
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.
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.
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↗. But 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.
Creation of Pseudo-Elements
The ::view-transition-old
pseudo elements are created in a pre-order depth first traversal 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 ::view-transition-old pseudo-elements are created during a pre-order depth-first traversal of the old DOM. When an element with a view-transition-name is encountered, its old image is rendered. 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.
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.
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.
Rendering Pseudo-Elements
Before the view transition starts, the pseudo elements are rendered on top of the current page. For cross-document view transitions, this might start in the middle of the page load process. But there are techniques to delay the start of the view transition.
The first pseudo-element that is rendered for a view transition is ::view-transition
. It has the size of the viewport and is full 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 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.
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 above all other elements of the page. As with regular elements, the z-index
property can be used to alter how the pseudo-elements hide each other. However, no regular page elements can be forced into or above the view transition layer, 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 during the transition. To manage this, either treat it as a feature or avoid such cases. Alternatively, you can include the obscuring elements in a view transition group, ensuring their images also move to the view transition layer, where they now have a chance to partially hide your other image as before.
Here you also can use z-index
properties to rearrange the images to your liking.
Just as pseudo-elements cannot be moved behind page elements, elements on the page won’t appear in front of the pseudo-elements. 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
.
Delaying Cross Document Transitions
With cross-document transitions the browser has to guess what would be a good time to start the view transition. It has do be able 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:
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.
Footnotes
-
In certain cases, the View Transition API may ignore the
view-transition-name
of an element. For instance, you can use the sameview-transition-name
multiple times within the same DOM, provided that all but one instance are not rendered (e.g., by setting theirdisplay
CSS property tonone
). This technique is utilized in the intra-document image morph examples on this site. ↩