Skip to content

Last updated: First published:

Styling View Transitions

The animations introduced by the View Transition API are controlled by CSS rules. The way to make the animations fit your expectations is to alter the default animation by overriding the styles for the pseudo elements.

To do this efficiently you should know what the View Transition API defines as defaults and how to easily change these styles if needed. To simplify styling, level 2 of the API introduced helpers in form of the view-transition-class property and the :active-view-transition* pseudo-classes.

Styling Tasks for View Transitions

There are three parts that you influence with CSS when it comes to view transitions:

  • Tell the View Transition API which DOM elements to setup for individual animations
  • Setup animations for elements participating in a view transition
  • Trigger CSS for things other than animations

Add DOM Elements to View Transition

If you want a DOM element to participate in view transitions with an individual animation, you assign a value to the view-transition-name CSS property of the element. The browser will then generate three or four pseudo-elements for each DOM element with a view transition name. These are a transition group, an image pair and at least one of the old or new image pseudos.

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

It is not important to have much imagination when choosing names. They can be the same as the id of an element or name of a singleton 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) styling to visibility: hidden or 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, no JavaScript needed. Check if your current browser already supports it. And don’t expect matching values on cross-document navigation. For that use case you need a JavaScript solution.

.tile {
view-transition-name: auto;
}

The browser automatically assigns the view transition name root to the :root element, i.e. the top-level <html> element for an HTML document. If you prefer another name, you can override it. If you do not want the <html> element to participate in the view transition with own pseudo-elements, you explicitly have to remove the root name by overriding it with none.

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.

Define Animations for View Transitions

For a single view transition name, x, there are different animations that you might want to style.

If your image-pair has both, an old and a new image, you can control how the old image is replaced by the new image during the transition. Three animations are involved:

  • the animation of the view-transition-old(x) pseudo-element,
  • the animation of the view-transition-new(x) pseudo-element and
  • the animation of the view-transition-group(x) pseudo-element

The first two typically work together in a cross-fade or in a combined effect that removes the old image and inserts the new one. The animation of the view-transition-group() aligns differences in size, transformation, and position of the old and new image.

If your ‘image-pair’ has only a single image, the browser places the view-transition-group() at the same position and does not define a morphing animation because there is nothing to align. You will still have the animation for the old or new image, respectively. Instead of a morph animation you have an entry animation if the old image is missing or an exit animation if the new image is missing.

Using an animation shorthand, CSS rules for the element with view transition name x could look like this:

redefining all animations with the shorthand
::view-transition-old(x) {
animation: 333ms linear both slide-out;
}
::view-transition-new(x) {
animation: 333ms linear both slide-in;
}
::view-transition-group(x) {
animation: none; /* switch off morph transition */
}

The animation shorthand is quite expressive. You can define several animations at once wit h different durations and start delays.

There might be good reasons for defining completely new animations for your view-transitions. But when you assign to shorthand properties, like animation, you completely override the definitions from the user agent stylesheet. And then you have to specify all values or be sure that the CSS default values fit your expectations.

If you only want to override the duration and the timing function of an animation defined by the view transition API, you might be better of with:

Redefine single animation properties but keep all others
::view-transition-group(x) {
animation-duration: 333ms;
animation-timing-function: linear;
}

That way you keep the former definitions and only change what is important to you. For an example on reusing browser-defined keyframes, see the notes on reusing user agent stylesheets.

Sometimes you want the animations for new and old images behave different for morph and entry/exit animations. this two cases can be distinguished with the :only-child CSS pseudo-class.

For example, assume you have an element that pops in with an entry animation and pops out with an exit animation. You do not want an animation if the element is on both sides of the transition, i.e. neither exit nor entry.

:is(::view-transition-old(x), ::view-transition-new(x)):not(:only-child) {
animation-name: none;
}

Styling Beyond Animations

While the typical task will be redefining animations when styling view transitions, you can also use view transitions to trigger other style changes. You can statically set the properties of the pseudo-elements created by the View Transition API. For example you could add a border.

::view-transition-old(x) {
border: 1pt solid red;
}

You can also style other DOM elements that are not created by the View Transition API, but keep in mind that those elements are typically hidden behind the view transition pseudo-elements during a view transition.

The User Agent Stylesheet

When the View Transition API inserts an animation, it has to give some default values to them. Here is what the API automatically assign in its user agent stylesheet for a view transition name x. In keyframe names, -ua stands for user agent.

Group / Morph Animation

If both, the old image and the new image for x exist, the API defines a morph animation from the old to the new image. The browser generates specific keyframes for each view transition group:

::view-transition-group(x) {
animation: 0.25s ease 0s 1 normal both running -ua-view-transition-group-anim-x;
}
@keyframes -ua-view-transition-group-anim-x {
0% {
backdrop-filter: <old backdrop filter>;
transform: <matrix to exactly cover the old element>;
height: <old height>;
width: <old width>;
}
100% {
backdrop-filter: <new backdrop filter>;
transform: <matrix to exactly cover the new element>;
height: <new height>;
width: <new width>;
}
}

The transform is used to move the deck with the new image on top of the old image from the old image’s size, transform, and position to the new image’s size, transform, and position. I.e. if the original element of the view-transition-name has some CSS transformation applied on the old or new page, like rotate or skew, this will also be honored by the generated transform.

Cross-Fade of the images

The typical cross-fade effect of view transitions is implemented by animations defined on the pseudo-elements for the old and new images. The definitions are the same for all view transition names.

::view-transition-image-pair(x) {
animation-duration: inherit;
animation-delay: inherit;
animation-fill-mode: inherit;
}
::view-transition-old(x) {
animation: <inherited duration> ease <inherited delay> 1 normal <inherited fillMode> running
animation-name: -ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter
}
::view-transition-new(x) {
animation: <inherited duration> ease <inherited delay> 1 normal <inherited fillMode> running
animation-name: -ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter
}
@keyframes -ua-view-transition-fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes -ua-view-transition-fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes -ua-mix-blend-mode-plus-lighter {
0% {
mix-blend-mode: plus-lighter;
}
100% {
mix-blend-mode: plus-lighter;
}
}

The inherited values come from the view transition image pair, which inherits them from the view transition group. -ua-mix-blend-mode-plus-lighter is only added if the image pair has both images. For some background on -ua-mix-blend-mode-plus-lighter see the section about mix blend modes.

Non-Animation Properties

Of course styling view transition pseudo-elements is not limited to animations. The browser’s user agent stylesheet also contains examples for such styling:

:root::view-transition {
position: fixed;
inset: 0;
}
:root::view-transition-group(*) {
position: absolute;
top: 0;
left: 0;
}
:root::view-transition-image-pair(*) {
position: absolute;
inset: 0;
}
:root::view-transition-old(*),
:root::view-transition-new(*) {
position: absolute;
inset-block-start: 0;
inline-size: 100%;
block-size: auto;
}

Selecting Pseudo-Elements …

… with Names

In the examples above you saw the patterns for addressing an individual pseudo-element: The view transition name is used as a parameter of the pseudo-element selector:

::view-transition-<pseudo-element>(<view-transition-name>) {
/* property definitions */
}

Beside addressing individual pseudo-elements it is also possible to address the pseudo elements of all view transition names at once using * instead of a view transition name:

::view-transition-<pseudo-element>(*) {
/* property definitions */
}

Also seen above: You can address the root of all view transition pseudo-elements, although it has no name:

::view-transition {
/* property definitions */
}

… with Classes

When you have CSS rules for a large number of view transition names, you can use the (*) pattern to address a pseudo-element for all transition names and use specific rules with the ((name-x)) pattern to handle the exception. There might be situations where you wish for a better developer experience. This is where view transition classes come in.

Similar to the view-transition-name property, you can assign view-transition-class values to DOM elements. While view transition names have to be unique in the DOM, view transition class values need not and typically will not be unique.

When looking at how the view transition classes are used, the similarity to CSS classes is rather obvious. You use them inside the pseudo-element selectors, prefixed by a dot (.). If multiple class names are used to identify a pseudo element, all must match. The following CSS rule can be used to address all view transition groups that have the nav-link view transition class:

::view-transition-group(*.nav-link) {
/* ... */
}

This is an example on how to assign the class:

#navbar a {
view-transition-class: nav-link;
}
#prev,
#next {
view-transition-class: nav-link some-other-class;
}

This might best be combined with dynamically (per script) added view transition names or tooling where you declaratively assign view transition names.

… with Types

Often you want to support different animations for a given view transition pseudo-element and select one of those alternatives depending on some condition. The concept that the View Transition API provides for this use case is called active view transition types.

While it is active, a view transition might have a set of identifiers called types that can be used in CSS pseudo-classes to select different rules. During the view transition, the set can be altered at any time by assigning to the types property of the viewTransition object. The initial set of types can be set when calling startViewTransition() or it might be specified using the types property inside a @view-transition rule.

Level 2 signature of startViewTranstion()
document.startViewTransition({types: ['boom'], update: changeTheDOM})

or

Setting types using CSS
@view-transition {
navigation: auto;
types: boom, var(--special)
}

The pseudo-class that can check for types is :active-view-transition-type(). It takes a comma separated list of types. If at least one of these types is set on the active view transition, the pseudo-class matches the documents root element.

Thus, the typical patterns to use :active-view-transition-type() are:

Directly prepend the pseudo-class to a pseudo-element selector …

:active-view-transition-type(type)::view-transition-group(main) {
/* ... */
}

… or equivalently use it with a nested CSS rule.

:active-view-transition-type(type) {
&::view-transition-group(main) {
/* ... */
}
}

There is an additional pseudo-class that does not take type parameters.

:active-view-transition {
/* ... */
}

This selector will match the the :root element while a view transition is active. You can use this for example to change the shape of the pointer during view transitions.

When you navigate forth and back through the pages of this site, you see an example on how types are used to select different animation.

  1. Go to the page about flickers during morph animations. You’ll see that it slides in from the right, as you go forward within the site’s order of pages.
  2. Select the page again from the global site navigation (have first to open it on mobile). You see a different effect.
  3. Press the browser’s back key or button to return to this page. The page slides in from the left as you go backwards in the site’s order of pages.

See the Turn-Signal documentation on how this effect is achieved and what pitfalls to avoid.

Conditional View Transitions

The pseudo-classes can not only be used to define different animations for different situations. It is also possible to let the existence of a view transition group depend on view transitions types. So on some transitions, an element can have its own animation and on others it is just part of some parent and its animation.

You can find an example for this behavior on this site. When you visit the page about flickers during morph animations and visit it directly again be selecting it in the site navigation, you will see that for navigations to the same page, the <main> section does not have its own view transition. Also when you click the images on that page, you start view transitions where the <main> section is not animated. See the Turn-Signal documentation on how this effect is achieved and how you could get even more specific and cancel single images instead of whole transition groups.

Prefix Pseudo-Elements with :root?

The user agent stylesheet example above is an excerpt from the View Transition Draft Spec. There the pseudo-elements are prefixed with the the :root pseudo-class selector.

The scope of view transitions is the whole document. Until this might get changed in future versions, all pseudo-elements that the API generates are children of the document’s root element, addressable using the CSS :root selector. So currently ::view-transition-group(x) and :root::view-transition-group(x) are the same thing. And even if some future might bring pseudo-elements that are rooted at different elements, :root::view-transition* will still select only pseudo-elements of the document root.

When you use these patterns in your own stylesheets, be aware that the specificities differ: :root::view-transition-group(x) with a specificity of (0, 1, 1) is more specific than ::view-transition-group(x) with a specificity of (0, 0, 1).

Now you might conclude that you also need to prefix your view transition pseudo-elements with :root if you want to override the defaults. That is not the case. The user agent stylesheet has the least important origin and is overridden by user stylesheet rules — independent of specificity.