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#hero
This 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:
:root.vt-expand.vt-route-list li#movie-$(movie_id) img
It would only apply when in the list
route of the expand
transition, and would replace the $(movie_id)
string with the parameter with the same name from either route – in this case the details
route.
Intercepting the navigation
window.navigation.addEventListener("navigate", e => { velvette.intercept(e, { handler() { render(); } });
});
We let velvette
intercept the NavigateEvent
, and apply the configuration we gave it.
And here we go, an expand animation:
Summary
CSS View-transition present a new world of opportunities with smooth and impressive UX. But there’s no need for it to be difficult to author. With velvette
, managing the unique view-transition-names
and passing parameters between navigations and elements hopefully makes the developer experience smoother, not just the user experience.
To see everything together, checkout the with-velvette branch.
You can view the final demo here:
Check out the live version.