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.
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.
View Transition Names
You tell the view transition API which elements should be animated by assigning view-transition-name
CSS properties to the HTML elements.
<main style="view-transition-name: main">
main { view-transition-name: main;}
The static user agent stylesheet provides one default name for the document, called root
.
: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;}
Escaping
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
.
At the start of the view transition, the view transition names have to be unique in the DOM. You can exempt an element from this uniqueness check by setting its (or one of its parent’s) styles to display: none
. For those elements, the browser won’t generate pseudos.
Auto-Generated Names
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 will also be possible to do this with only CSS by using the auto
value, no JavaScript needed.
.tile { view-transition-name: auto;}
Currently, this only works in Safari.
Check if your current browser already supports it. Setting the value to auto
copies the name of the id
attribute if defined. Otherwise, it makes up some name like -ua-auto-....
.
Using auto
with id
can be used to define elements with the same view transition name for cross-document navigation. But dynamically generated names have to always be different↗ for two documents.
Even if it were widely supported, would would not recommend auto
right now for cross-document view transitions. There are alternatives that work cross-browser and also for cross-document navigation.
Dynamic Names
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.
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 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.
Important Exception
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.
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.
::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.
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.2
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 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.
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.
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 were created and 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 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 used in the href
attribute does not exist on the page, view transitions will wait for the whole page to load before they start. But be aware that the browser might cancel view transitions anyhow if that takes too long.
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.
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. ↩ -
In a way that will continue to work once nested view transition groups are available. ↩