Making CSS View Transitions Easy with Velvette

Learn how to effortlessly implement smooth CSS view-transitions with Velvette, a useful library designed to tackle common challenges and enhance user experiences in web applications.

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:

  1. 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.
  2. 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.
  3. Choosing how to style the transition based on a navigaiton requires quite a lot of broilerplate JS code.
  4. 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.

Noam Rosenthal

Noam is a software engineer at Google, working on Chromium & web standards, in the area of fast & smooth navigation experiences. He's currently co-editing the CSS view-transitions spec.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!