We recently released CSS view-transitions in Chrome. Yay! Soon after, people started innovating with it, creating cool and smooth experiences.
As it so happens when we release a web feature, patterns emerge of how to use it, and people tend to run into similar challenges. This post will show you examples of how to tackle those challenges with a library called Velvette.
CSS view-transitions in a nutshell
CSS view-transitions allow animating between two unrelated states of a document, by giving names to elements in the old and new state, capturing those elements’ snapshots into images, and animating those images in pseudo-elements. See this article for a good starter kit.
The missing pieces
The following is a summary of (a few) issues that people found difficult to do with view transitions:
CSS view-transitions work by having a unique name for each participating element, shared between the old state and new state. In many cases, generating those names is tedious as every participating element has to have a different one.
View-transitions are scoped for the whole document, so defining a view-transition-name for an element would capture that element every time there is a transition ― this creates redundant captures in pages that contain more than one view-transition.
Choosing how to style the transition based on a navigaiton requires quite a lot of broilerplate JS code.
All these problems come together when we try to create an animation between a list and details page, which is a common use case for view-transitions: setting the view-transition-name only on the relevant elements at the correct time takes careful precision and can otherwise be bug prone.
Velvette
Velvette is a library that works on CSS view-transitions and provides utilities that help with these challenges. It’s built as an add-on, you can slab it on an existing site and configure it to make that page animate between states. Progressive enhancement!
Let’s add animations to a little movies app
The movies app (or more like little part of an app) is at https://github.com/noamr/velvette-codrops. It uses a snippet of static data from TMDB, and lets you do two things:
Sort a list of movies by name/ID/release date
Click a movie to see its details, click it again (or press “back”) to go back to the list.
Download the repo and run a small web server. See how switching between list and movie, and changing the sort order, both occur instantly without an animation.
In this demo we’ll show how to:
Animate the list sorting, where every element animates into place.
Animate the navigation between the movie list and details, such as the hero image expand/shrinks.
Installing Velvette
Installing velvette works in the standard way, by adding it with npm or by using a script tag.
Note: try this only in Chrome for now…
Add this to index.html:
<script src=\”https://www.unpkg.com/velvette@0.1.10-pre/dist/browser/velvette.js\”></script>
This will put a Velvette class on your window object, and we can get going to use it for smooth view-transitions.
Sort animation
Let’s start with the sort animation.
In index.js, we have a line responsible for sorting when one of the radio buttons is clicked:
document.forms.sorter.addEventListener(\”change\”, () => render());
Let’s start by having a simple transition:
document.forms.sorter.addEventListener(\”change\”, () => {
if (\”startViewTransition\” in document)
document.startViewTransition(() => render());
else
render();
});
This creates the default fade animation – the whole page fades. It’s a start, but not what we’re after.
Replace this line with a line that animates the sort, with a unique view-transition-name for each of the elements:
document.forms.sorter.addEventListener(\”change\”, () => {
Velvette.startViewTransition({
update: () => render(),
captures: {
\”section#list li[:id]\”: \”$(id)\”
}
});
});
This still calls the render() function on every sort change, but also invokes velvette to start a view-transition, where every item that matches the selector section#list li[id] would have the li‘s ID assigned to its view-transition-name. The [:id] part captures the ID attribute as it goes through the selector change, and then applies it to the name in the end.
Now refresh the page and try to change the sort. Voila, a sort animation! (Could be nicer, feel free to do the design work…)
Expanding/shrinking the image
Now let’s get to the other part, expanding/shrinking the hero image when going between the list and details view. Note that the current code that switches between them in index.js uses the navigation API:
window.navigation.addEventListener(\”navigate\”, e => {
e.intercept({
handler() {
render();
}
});
});
To make that navigation trigger a view transition, we create a velvette configuration that defines how different routes in our app behave in terms of CSS view-transitions:
const velvette = new Velvette({
routes: {
details: \”?movie=:movie_id\”,
list: \”?list\”
},
rules: [{
with: [\”list\”, \”details\”], class: \”expand\”
}, ],
captures: {
\”:root.vt-expand.vt-route-details img#hero\”: \”movie-artwork\”,
\”:root.vt-expand.vt-route-list li#movie-$(movie_id) img\”: \”movie-artwork\”
}
});
Let’s go over what’s in this configuration:
Routes
We define two routes, details and list. Those routes are URL patterns. Note that the details route captures a movie_id parameter.
Rules
Rules define which navigations should trigger a view-transition, and which class to add for this view-transition. In this case, we declare that any navigation between list and details (in either direction) should trigger a view-transition, and add the expand class (which would be prefixed as vt-expand.
Captures
Like in the sort example, we add captures: a map between a selector and a generate view-transition-name. In this case, we want a single view-transition-name called movie-artwork to be applied both to the hero image and to the correct thumbnail (and only the correct one), but not at the same time – otherwise the transition would be skipped.
The first selector takes care of the hero image:
:root.vt-expand.vt-route-details img#heroThis would apply only when we’re capturing the expand transition, and only when we’re in the details route.
The second selector takes care of the thumbnail: