The ::view-transition-new() pseudo-element applies styles to the new snapshot of an element during a view transition. This means styles on this pseudo-element work as entry transitions whenever a “new” page has been loaded.
A fun fact about this visual snapshot is that, according to the specification, it is a replaced element (e.g <img>, <iframe>, and embedded links) and can be manipulated with the same properties that style replaced elements, like <img>.
::view-transition-new(item) {
animation-name: fadeIn;
}
@keyframes fadeIn {
to {
opacity: 0;
transform: translateY(-10px);
}
}
The ::view-transition-new() pseudo-element is defined in the CSS View Transitions Module Level 2 specification.
::view-transition-new(<pt-name-selector>);
where…
<pt-name-selector>: Selects a view transition name and can either be:
*: Targets all the view transition groups on the page<custom-ident>: A valid CSS identifier that is not a keywordroot: Targets the :root::view-transition-group(*) in the browser which represents the default snapshot group created by the browser for the whole transition on the entire page. This group also includes all elements that have not been assigned to a specific view transition name via the view-transition-name property./* targets all the view transition groups */
::view-transition-new(*) {
animation: slideOut 0.5s ease-in;
}
/* targets the whole page */
::view-transition-new(root) {
animation: slideLeft 0.15s ease-out;
}
/* select a specific view-transition-name,
e.g. view-transition-name: goat;
*/
::view-transition-new(goat) {
animation: slideLeft 0.53s ease;
}
The ::view-transition-new() pseudo-element is used to apply certain animations to incoming elements. This means we are targeting the new element during a view transition.
.big-section {
view-transition-name: faces;
}
/* view transition animation applied to only section with view transition name "faces" */
::view-transition-new(faces) {
animation: scaling 2.5s;
}
@keyframes scaling {
from {
transform: scale(1.5);
}
to {
transform: scale(1);
}
}
Or if you want to target the whole page, including elements without a view transition name, use root:
.big-section {
view-transition-name: faces;
}
/* view transition animation applied to the whole page */
::view-transition-new(root) {
animation: scaling 2.5s;
}
@keyframes scaling {
from {
transform: scale(1.5);
}
to {
transform: scale(1);
}
}
::view-transition-new() is a pseudo-element of the view transition tree itself, not of DOM elements. You style it standalone, without element selectors. Using section::view-transition-new(*) is syntactically valid but semantically redundant. The view transition pseudo-elements exist outside the DOM hierarchy, so the section selector has no effect.
/* This will not work */
section::view-transition-new(*) {
animation: moveIn 0.24s;
}
Instead, we give an element a view-transition-name which is then passed inside ::view-transition-new()
section {
view-transition-name: big-section;
}
::view-transition-new(big-section) {
animation: moveIn 0.14s;
}
::view-transition-new()One major difference between ::view-transition-new() and ::view-transition-old() is that the former applies entry animations to outgoing elements. So, to make the best use out of animations for your new page, use ::view-transition-new(). Here’s a demo specifically showing the entry animation for only the incoming pages.
::view-transition-group(.article-title-class) {
animation-duration: 1.3s;
}
::view-transition-new(.article-title-class) {
animation-name: fadeIn;
}
@keyframes fadeIn {
from {
opacity: 1;
transform: rotate(120deg) translateY(40px);
}
}
The animation launches the header from 120deg to 0deg while translating it to the 40px on the y-axis of the screen. Again, this would only apply to the incoming elements.
To represent how view transitions work, the specification provides what is known as the View Transition Tree, which gives us a visual representation of a view transition containing all the view transition pseudo-elements:
::view-transition
├─ ::view-transition-group(name-1)
│ └─ ::view-transition-image-pair(name-1)
│ ├─ ::view-transition-old(name-1)
│ └─ ::view-transition-new(name-1)
├─ ::view-transition-group(name-2)
│ └─ ::view-transition-image-pair(name-2)
│ ├─ ::view-transition-old(name-2)
│ └─ ::view-transition-new(name-2)
... and so on
See the ::view-transition-new() pseudo-elements inside each ::view-transition-group()? Well, they will be created inside a view transition group for each view-transition-name we define.
That means we can have as many ::view-transition-group()s, ::view-transition-image-pair()s, ::view-transition-new()s ::view-transition-old()s according to the number of view-transition-names present.
Almost any animation can be applied to this, but remember how we mentioned at the start that the replaced element is an <img>? Well, this means properties like object-fit, clip-path, filter, transform, and opacity are perfect for styling ::view-transition-new().
Here’s a demo showing different CSS properties used to animate page transitions using a single webpage:
That looks really clean! To achieve it, we’ll first need to give each image a view-transition-name.
img {
/* ... */
view-transition-name: match-element; /* Important to create a unique identifier for all images */
view-transition-class: hero-img;
}
We use the match-element value inside view-transition-name for the <img> element to generate unique names for each <img>. match-element is a special keyword that tells the browser to automatically generate unique view-transition-name values for each element. For this to be useful, you pair it with view-transition-class to group those elements under a shared class for styling.
Then, we set the animation duration of each image to be 2.6s using the ::view-transition-group() pseudo-element.
/* Group animation duration */
::view-transition-group(.hero-img) {
animation-duration: 2.6s;
}
Finally, we give the element a shrink animation when moving from the old state to the new state using the ::view-transition-old() pseudo-class.
/* Group animation duration */
::view-transition-group(.hero-img) {
animation-duration: 2.6s;
}
/* OLD snapshot animation */
::view-transition-old(.hero-img) {
animation-name: shrink;
}
@keyframes shrink {
from {
clip-path: circle(100% at 50% 50%);
transform: scale(1);
}
to {
clip-path: circle(0% at 50% 50%);
transform: scale(0.9);
}
}
Another important detail that makes this work is this JavaScript:
if (document.startViewTransition) {
document.startViewTransition(() => {
img.src = newSrc;
});
} else {
img.src = newSrc;
}
The code above checks if your browser supports view transitions, and if it does, it calls the startViewTranstion() function on the document object and passes in an anonymous function that applies a transition only when a new photo is passed. If the browser doesn’t support view transitions, it just changes the photo without any animation.
In fact, notice how both ::view-transition-old() and ::view-transition-new() made this work. But, how would it look if we only applied ::view-transition-new()? I mean, ::view-transition-new() is our focus, right? Well, I’m glad you asked. Here’s the demo:
It would still look the same! And that’s because we applied almost the same animation to both of them for consistency, so we still see the entry animation. Pretty sick, right?
To better understand the code above and how view transitions work, take the following example as a case study. We have several list items using a common <ul>. If we wanted to apply a simple view transition animation whenever we click a “Reverse list” button, we’d need to give each list item <li> a view transition name:
.item:nth-child(1) {
view-transition-name: item-1;
}
.item:nth-child(2) {
view-transition-name: item-2;
}
.item:nth-child(3) {
view-transition-name: item-3;
}
/* ... */
.item:nth-child(15) {
view-transition-name: item-15;
}
Now, imagine this list wasn’t even 15, but 100! That would be a LOT of view-transition-names to write out. The match-element feature helps to eliminate the time spent writing unique view-transition-names and tells the browser, “Hey, you do it for me.” So instead, we can simply write:
.item {
view-transition-name: match-element;
view-transition-class: item;
}
The CSS view-transition-class is important because we want to style every single list item with the same animation. That means that, instead of having multiple ::view-transition-group()s for each ::view-transtion-name, we can just give all the elements a ::view-transition-class of item and call all of them out using the single class name of item:
.item {
view-transition-name: match-element;
view-transition-class: item;
}
/* When using view-transition-class, reference it
in ::view-transition-group() with a dot prefix
(e.g., .item), similar to class selectors,
but in this specific context.
*/
::view-transition-group(.item) {
animation-duration: 5.5s;
}
Finally, we have ::view-transition-new() applying animation on the new snapshot when it transitions from the old state to the new state:
::view-transition-new(.item) {
animation-name: fadeOut;
}
@keyframes fadeOut {
to {
opacity: 0;
}
}
One last thing. In order to perform this on the same page on-button click, we have:
button.addEventListener("click", () => {
document.startViewTransition(() => {
// Some function to reverse elements
reverse();
});
});
Notice the document.startViewTransition() function? That applies the view transition to each list item when the “Reverse list” button is hit. The reverse() function reverses the list as we see fit, and it is placed inside the startViewTransition() function callback. In fact, without this, we would not trigger the view transition animation on the same page. So take note of this.
With this, we can easily change the transition. For example, applying ::view-transition-new() with a fadeIn animation would look like this:
::view-transition-new(.item) {
animation-name: fadeIn;
}
@keyframes fadeIn {
from {
opacity: 0;
width: 0;
}
}
Looks cool, right? That’s the power of view transitions!
Here’s another bonus animation that involves ::view-transition-old() and ::view-transition-new(). We’re simply toggling between two cards smoothly using filter: and blur()translateY() for added movement:
I also customized our previous demo to have an option to randomize the order of items for more fun, so try it out!
There are two types of transitions that you can perform page-wise: (1) the in-page transition and (2) the cross-page transition.
For the in-page transition, you perform the transition with elements present on the same page. This can be simply done with view-transition-name, ::view-transition-new() (and even ::view-transition-old(), view-transition-class, and ::view-transition-group() as specified in Geoff’s article on “Toe Dipping Into View Transitions.”
The in-page transitions happen within the same document page, hence the name. In comparison, cross-page transitions happens between two pages of the same origin.
The major code difference between in-page transitions and cross-page transitions is that the @view-transition at-rule is not present in in-page transitions:
@view-transition {
navigation: auto;
}
This single line of code enables cross-fading between two pages. When combined with the technique we learned before, you have this:
To make it, we first set the view-transition-name to match-element to create unique names for each progress bar on the pages marked with the .progress-bar class. We also set view-transition-class property to progress so we can select it later.
.progress-bar {
view-transition-name: match-element;
view-transition-class: progress;
}
Then, inside ::view-transition-group() and ::view-transition-new() we can specify the animation we want to use during the transition:
::view-transition-group(.progress) {
animation-duration: 0.9s;
}
::view-transition-new(.progress) {
animation-name: fadeIn;
}
@keyframes fadeIn {
from {
opacity: 0;
}
}
Make sure to note that animations applied to the new elements are controlled by the ::view-transition-new() pseudo-element.
During an active view transition, the page is frozen and unresponsive until the animation completes. If you set very long animation durations, users cannot interact with the page during that time. This is why it’s important to keep transitions brief and respect prefers-reduced-motion.
Notice in this demo, when the animation duration is increased to 5.8s you CANNOT interact with any element on the page. The button click doesn’t even work.
This can be a frustrating experience for your users, so before you think of placing in those fancy long animations, maybe consider your users first and keep things brief and enjoyable.
::view-transition-new()Lastly, remember that animations can make users with vestibular disorders sick. Remember to only enable transitions for users with prefers-reduced-motion: no-preference:
.item {
@media (prefers-reduced-motion: no-preference) {
view-transition-name: match-element;
view-transition-class: item;
}
}
This way, the name would only be given when the user doesn’t have any preference set for reduced motion, and if it is set, the animation defaults to a simple fade in and out.
That said, short animations (under 200ms) might not be noticeable to some users, so there’s a balance between accessibility (reducing motion) and UX (providing feedback).
The CSS ::view-transition-new() pseudo-element is defined in the CSS View Transitions Module Level 1 specification, which is a stable standard.
It is also included in the CSS View Transitions Module Level 2 specification with new features like match-element and view-transition-class. This is in Editor’s Draft, which is still in progress as of this writing.
View transitions continue to evolve, with different features at different levels of standardization. Here’s the current support status:
The view-transition-class property:
And specifically, cross-document view transitions:
@view-transition { navigation: auto; }
::view-transition { position: fixed; }
::view-transition-group(transition-name) { animation-duration: 1.25s; }
::view-transition-image-pair(root) { animation-duration: 1s; }
.element { view-transition-name: image-zoom; }