Dec 19

View Transitions

Animating DOM state changes no longer requires a mountain of code thanks to the new View Transitions API.

By Chen Hui Jing

Web applications these days are becoming more complex thus, having smooth transitions between view states as a form of functional animation has the practical benefit of reducing cognitive load, preventing change blindness, and establishing better recall in spatial relationships.

Trying to animate between different DOM states used to involve a serious amount of Javascript and CSS because of the number of factors we had to take care of. In addition to the animations themselves, we also had to worry about handling the loading and rendering of content within the different states and potential accessibility issues.

The View Transitions API was proposed in the CSS working group back in 2021 to allow transition animations when navigating between different application views. A lot of work has gone into this, and the specification is now at Candidate Recommendation status.

How does it work?

Generally, if we want to animate something smoothly, it must have a start state and an end state, then we have to figure out how to fill in the gap between the two states in a gradual manner. As an oversimplification, CSS transitions and animations today generally work because the browser can compute the values of the in-between states of an element.

If we want multiple DOM nodes to participate in the same animation, things become very tricky because the computation required increases significantly. The proposed solution is to handle the complicated in-between state in a separate layer altogether with the help of pseudo-elements.

The visual transition is done using a static visual capture of the original state and a live capture of the new state, represented as a tree of pseudo-elements.

::view-transition
├─ ::view-transition-group(name)
│  └─ ::view-transition-image-pair(name)
│     ├─ ::view-transition-old(name)
│     └─ ::view-transition-new(name)
└─ …other groups…

It all starts with the startViewTransition() method, which returns a ViewTransition object when called. The state at this point is captured as the “old” state, and rendering is paused.

The startViewTransition() takes a callback function as an argument, called asynchronously once the old state has been captured. Once the promise is fulfilled, the new state of the document is captured as the “new” state, and now we have the two states necessary for a smooth transition to happen.

What’s particularly interesting about how this feature is implemented is that “a failure to create a visual transition, which can happen due to misconfiguration or device constraints, will not prevent the developer’s UpdateCallback being called.”

As previously mentioned, the startViewTransition() method takes a callback function as an argument, and this function will always be called even if the animation does not happen due to reasons. I think the idea is pretty smart, and I highly suggest reading the specification because it’s a very readable explanation of the details.

View Transition API in action

These days, a common pattern we see on the web is data that gets populated after the page has loaded. It could be a user interaction that triggers a change in the content with new content that hadn’t existed in the DOM previously. We can animate such a content change using the View Transitions API.

For our example, we'll build an image gallery and have a scenario where clicking on a thumbnail will populate the main display with a larger version of the image.

A preview of the image gallery where a large area on the left is displaying a title slide for 'fluffy Philharmonic' and on the right is a vertical stack of four images of cute illustrated animals playing string instruments.

The basic markup for the display looks something like this:

<figure>
<img class="main-image" src="https://i.imgur.com/j6tUehw.jpg" alt="Mamamoo World Tour MYCON">
<figcaption class="image-heading"></figcaption>
</figure>

Our list of thumbnails looks like this:

<ul class="thumbnails">
<li>
<a href="#">
<img class="thumbnail" src="..." alt="Solar">
</a>
</li>
<!-- additional list items -->
</ul>

Let’s start off with our callback function (to be passed to the startViewTransition() method):

const mainImage = document.querySelector("figure img");

const displayNewImage = () => {
mainImage.src = clickTarget.src;
};

Because this is just a demo, this function sets the main image source to the thumbnail that was clicked. In an ideal situation, we would have wanted to have optimized different images because of performance but let’s live with this for now.

As not every browser currently supports the View Transitions API, we need to include a check for support. The code for the checking View Transition support looks like this:

if (!document.startViewTransition) {
displayNewImage();
return;
}

const transition = document.startViewTransition(() => displayNewImage());

The default ViewTransition is a cross-fade effect, where the old view animates from opacity: 1 to opacity: 0, and the new view animates from opacity: 0 to opacity: 1. The browser does this out-of-the-box as the default animation. If this effect is sufficient, no additional CSS is necessary.

See the CodePen.

The entire page is involved in the transition for this most basic implementation. But we can also target specific elements to be animated separately from the rest of the page by assigning a view-transition-name to the element.

We can try something more than a simple cross-fade. Let’s make it seem like the image is growing out from each respective thumbnail when the thumbnail is clicked on with the help of some additional CSS on the view transition pseudo-elements.

::view-transition-old(main-image),
::view-transition-new(main-image)
{
transform-origin: 100% var(--originY, 50%);
}

::view-transition-new(main-image) {
animation: 400ms ease-out both grow;
}

figure {
view-transition-name: main-image;
}

@keyframes grow {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}

We need to update the markup and Javascript a little update the transform-origin property value when the thumbnails are clicked. Let’s add a data-index attribute to each thumbnail, which can be used to calculate the updated value so the main display image starts animating from the position of each thumbnail.

<ul class="thumbnails">
<li>
<a href="#">
<img class="thumbnail" src="..." alt="Solar" data-index="0">
</a>
</li>
<!-- additional list items -->
</ul>

And update the Javascript to modify the --originY custom property value that controls the transform-origin of the scale transformation along the y-axis.

const displayNewImage = () => {
mainImage.src = clickTarget.src;
document.documentElement.style.setProperty(
"--originY",
clickTarget.dataset.index * 25 + 12.5 + "%"
);
};

The end result of all that looks like this:

See the CodePen.

We can target other elements to be animated separately by giving them their own view-transition-name value. The view-transition-name can be anything at all, but it needs to be unique and cannot be none, as that would result in no transition at all.

Maybe we want an image header to go with the main display. Again, for demo purposes, I’m just going to use the alt text from the thumbnail, but the content could come from somewhere else. The point is the content is changing. Let’s update the displayNewImage callback:

const displayNewImage = () => {
mainImage.src = clickTarget.src;
imageHeading.innerHTML = clickTarget.alt;
document.documentElement.style.setProperty(
"--originY",
clickTarget.dataset.index * 25 + 12.5 + "%"
);
};

The animation is currently on the <figure> element, which contains the <figcaption> element where the text is displayed, so the scale effect will include the <figcaption>. But the View Transitions API allows us to separate the <figcaption> animation from the <figure>, and to me, that’s a huge plus.

Once a view-transition-name is assigned to the <figcaption>, it will now cross-fade when the <figure> scales. You can also customize the animation for the <figcaption> with CSS. There are so many possibilities!

See the CodePen.

Can we use it today?

As it was put forth by the folks at Google, Chromium has had a working implementation of the View Transitions API since version 111. Both Firefox and Webkit are currently working on their implementation of this feature, tracked in Bug 1823896 on Firefox Bugzilla and Bug 259055 on Webkit Bugzilla, respectively.

Wrapping up

Hopefully, this post manages to highlight the potential of View Transitions and inspires you to explore more and give it a try yourself.

References

Chen Hui Jing

Chen Hui Jing

Chen Hui Jing is a self-taught designer and developer living in Singapore, who loves CSS more than most people. She can be found spanning routes in some local climbing gym.

Chen selected Red Panda Network for an honorary donation of $50

Red Panda Network

Red Panda Network is committed to the conservation of wild red pandas and their habitat through the education and empowerment of local communities.