Last updated: First published:
Check and Control the Image Animations!

Hello and welcome back to Fun with View Transitions, the series where we dive into the wonders of the View Transition API!
In the first episode, we set up a multi-page site with built-in view transitions for navigation. Today, we are taking it a step further by exploring the defaults animations used during page transitions and learning how to fine-tune them to fit your needs.
Here’s what we’ll cover:
- Examining the pseudo-elements involved in view transitions
- Breaking down the default cross-fade effect
- Configuring a custom cross-fade
- Crafting animations that go beyond cross-fades
What will change on the example site?
Let’s jump in!
Meet the Pseudo-Elements
Our journey begins by uncovering what happens behind the scenes when navigating between pages on a multi-page site with cross-document view transitions enabled.
By applying the @view-transition { navigation: auto }
CSS at-rule to all pages of your site, the View Transition API dynamically generates five pseudo-elements and injects them as a sub-tree within the document root.
<html> ::view-transition ::view-transition-group(root) ::view-transition-image-pair(root) ::view-transition-old(root) ::view-transition-new(root)
These pseudo-elements actually cover your entire viewport, concealing the original page behind them.
- The
::view-transition-old(root)
is a snapshot image of the viewport as it appeared right before you navigated away from the old page. - The
::view-transition-new(root)
is an image that shows the new page’s viewport. Nice thing here is, that it is not a static snapshot. While it exists it dynamically reflects any changes happening on the new page. - The remaining three pseudo-elements are simple containers, similar to
<div>
elements. We’ll revisit their role and how they animate in a future episode.
Despite their unconventional names, these pseudo-elements behave like any other HTML pseudo-elements, making them fully customizable with CSS.
Cross-Fade Rules
We can select them like a <div>
or <img>
elements and animate them. For example, we could fade out the old image with the following CSS rule.
We can select and animate these pseudo-elementsJ just like <img>
or <div>
elements. For instance, we can fade out the old image using the following CSS rule:
::view-transition-old(root) { animation: 250ms ease 0ms 1 normal both -ua-view-transition-fade-out; animation-duration: inherit;}
Fortunately, we don’t need to define this ourselves. This exact1 animation is already applied by the View Transition API through the browser’s built-in user-agent stylesheet as the default behavior.
Similarly, the API provides a built-in animation to fade in the new image:
::view-transition-new(root) { animation: 250ms ease 0ms 1 normal both -ua-view-transition-fade-in; animation-duration: inherit;}
The animation duration of 0.25s is set on the ::view-transition-group(root)
and inherited through the ::view-transition-image-pair(root)
. This explains why we noted “The animation lasts 250 milliseconds using CSS’s default ease timing function.” at the end of Episode One.
Can We Modify those Values?
Absolutely! Since it’s all pure CSS, there’s no obscure magic here. As we discussed in Episode One, you know where to place global CSS rules for your site.
To override the default animation and change the duration of the view transition, add this definition to your stylesheet:
::view-transition-group(root) { animation-duration: 10s;}
Okay, 10 seconds is far too long for a practical transition, but this exaggerated value serves a few purposes:
-
It makes the effect obvious enough to demonstrate the point clearly.
-
It highlights the thoughtfulness behind the default rules, crafted for consistency and developer experience, by letting you change the duration of two animations with a single change.
-
It gives you time to inspect the pseudo-elements in your browser’s developer tools. These elements are temporarily attached to the
<html>
element and only exist for the duration of the animation. -
This also gives you first-hand experience with one limitation2 of the View Transition API: although you can see the new page during the animation, you can’t interact with it until the transition ends.
Manually setting long animation durations isn’t the best way to inspect or debug view transitions, though. In a later episode, we will explore how to delay and pause view transitions using your browser’s developer tools and the Inspection Chamber.
Before you move on, don’t forget to reset the overall animation duration to a more practical value, like the default 0.25 seconds or perhaps 0.2 seconds if you prefer a snappier cross-fade effect.
A Quick Tip: When experimenting with animation parameters, resist the urge to push the fade-out and fade-in out of sync. Using different timings for concurrent opacity changes can cause odd visual effects when the browser combines the partially transparent images.
Are There Alternatives to Cross-Fade?
Definitively! Let’s replace the cross-fade with a more dynamic animation where the old image fades away and the new image slides in with a subtile upward motion:
::view-transition-old(root) { animation-duration: 150ms;}::view-transition-new(root) { animation: 150ms 100ms both slide-up;}@keyframes slide-up { from { opacity: 0; transform: translateY(10vh); }}
In this example:
- Both fade animations are sped up to just 150 milliseconds.
- The animation for the new image is delayed by 100 milliseconds, so it begins only after the old image is almost fully faded out.
- The combined effect creates a smooth animation that runs for 0.25 seconds overall.
We also introduce a custom animation, slide-up
, which blends a fade-in effect with slight upward movement. The possibilities are endless: move, stretch, rotate in 3D, or anything else CSS can do with images! All of it works seamlessly with the pseudo-elements provided by the View Transition API.
You can even combine these animations with CSS transitions and the Web Animation API, opening up even more creative possibilities. But we’ll leave that for another episode.
Ready for another experiment? Try adding the following easing function to your animation for the new image
linear(0, 1.2 50%, 0.8 75%, 1)
and increase all duration values for a slower effect.
If you want, you can compare your styles against the sample solution↗.
Here is what it looks at fun-with-view-transitions.pages.dev↗ with those changes applied.
What is “(root)”?
Now it is time to ask why all the pseudo-element names we saw in this episode end with “(root)”. The identifier root
is a view transition name.
View transition names tell the View Transition API to generate a ::view-transition-group
pseudo element for the DOM element the name is assigned to. So with more view transition names, there will be multiple :view-transition-group
elements and the view transition name is used to distinguish them. Therefore, the view transition names, for which the API generates groups, must be unique on a page. The API won’t generate groups for invisible elements3. So the rule in truth is: View transition names for visible DOM elements must be unique on a page.
That the whole view port is assigned the root
name is another default from the useragent stylesheet:
:root { view-transition-name: root;}
As with any thing from the useragent stylesheet you can override it with a different name, and especially with the special ident none
, which will remove the view-transition-name from the root element and prevent the generation of a view transition group for the document’s <html>
element.
So what are these additional groups then? This, my dear reader, is the topic of the next episode!
Thank you for joining on this journey through the exciting world of view transitions. Until next time, keep animating, keep experimenting, and most importantly, keep having fun… with view transitions!
Footnotes
-
Well, not exaclty this, but very close. See for yourself!! ↩
-
Rumors suggest that scoped view transitions might eventually restore some interactivity during transitions. Stay tuned for updates! ↩
-
“invisible” here means
display: none
. Just being clipped or hidden byvisibility: hidden
,overflow: ...
or being obscured by other elements does not hinder group generation. ↩