Skip to content

noamr/velvette

Repository files navigation

Velvette

Making it easier to author CSS View Transitions

For the docs go here

Tl;dr

A small JS library that implements common patterns on top of CSS view-transitions:

  • Useful temporary classes while the transition is active
  • Unique name generation
  • Share pseudo-element styles between captured elements
  • Respond to navigations
  • Animate between list and details

To install

npm install velvette
import {Velvette, startViewTransition} from "velvette";

Or directly in the browser:

<script src="https://www.unpkg.com/velvette@0.1.6/dist/browser/velvette.js">
<script>
// You now have `Velvette` in your window object.
Velvette.startViewTransition(...)
const velvette = new Velvette(config);
</script>

Overview

CSS View Transitions

CSS View Transitions, released in 2022, allow smooth transitions between different states of the same page, and soon they would allow smooth transitions between different documents of the same origin.

What's Missing

As it happens with web platform features, the patterns for using the feature emerged after its release. We've found that people who use CSS view-transitions often end up running into similar challenges and gotchas, having to write similar broilerplate code to overcome them.

In comes Velvette

Velvette is a library that allows you to specify in a declarative way how your transitions should behave, in isolation or as a response to a navigation, and then apply the declaration to a particular ViewTransition, NavigateEvent, or use it to handle cross-document ("MPA") navigations.

Why not implement these patterns in the browser instead?

The Chrome team is definitely doing that! But implementing browser features takes time, and requires a consensus among many players. Implementing them now in JS allows authors to use them ergonomically today, and also gives us an experimentation ground to new ideas before they mature enough to go into the spec.

General Design

Velvette handles these features by attaching to the ViewTransition's promises, and changing the DOM in the following ways:

  • Add temporary classes to the document element while capturing the transition states.
  • Add constructed styles to the document while the transition animating.
  • Generate and set view-transition-name properties according to rules.
  • Perform all of the above based on same-document navigations (using the Navigation API) or cross-document navigations.

Features

(From simple to complex)

Respond to old/new transition states

See #9424. Sometimes we want to style the transition based on "old" and "new" states.

Velvette does this automatically when a transition is extended, by setting temporary classes vt-old and vt-new on the document element:

import {startViewTransition} from "velvette";
startViewTransition({update: updateTheDOMSomehow})
:root.vt-old #foo { view-transition-name: item; }
:root.vt-new #bar { view-transition-name: item; }

Specify different transitions in the same page

See isue #8960

When there are multiple transitions in the same page, it's hard to define what is captured, often leading to over-capturing unnecessary elements.

To specify a temporary class, extend a ViewTransition like so:

import {startViewTransition} from "velvette";
startViewTransition({update: updateTheDOMSomehow, classes: ["slide-main"]});
:root.vt-slide-main main {
    view-transition-name: main;
}

Generate unique view-transition-names

See #8320.

Some view-transitions operate on many elements in a page, rather than on a given set of elements. A common use-case for this is animated list-sorting. Setting unique view-transition-name properties on all the participating elements could become a tideous job.

Velvette implements this in the form of attribute substitution:

<main>
    <div class="box" id="box1"><img src="..."></div>
    <div class="box" id="box2"><img src="..."></div>
</main>
import {extend} from "velvette";
extend(viewTransition).capture(".box[:id] img", "$(id)");

This would generate the following temporary CSS (as inline styles) while capturing the transition:

.box#box1 img { view-transition-name: box1 }
.box#box2 img { view-transition-name: box2 }

Share pseudo-element styles between captured elements

See #8319

When we capture multiple elements under different names, we might want to apply the same styles to their corresponding pseudo-elements.

For example, in the example above, we might want all the boxes to animate for a 1 second duration.

We do this by extending the view transition with a style that matches a capture, like so:

startViewTransition({
    update,
    captures: {".box[:id] img": "$(id).any-box"},
    styles: {"::view-transition-group(.any-box)": {animationDuration: "1s"}}
})

This will generate styles for all the captured elements that fit the class, e.g.:

::view-transition-group(box1) { animation-duration: 1s }
::view-transition-group(box2) { animation-duration: 1s }

Responding to navigations

See issue #8685, issue #8925, issue #8683, and others.

A common use-case for CSS view-transitions is responding to a navigation, whether it's in the same document ("SPA") or across same-origin documents ("MPA").

To help with this, constucting a Velvette object with a certain configuration lets you declare the rules to how different navigations should be handled.

A navigation consists of an old URL, a new URL, and a navigation type, which could be "push", "replace", "reload", "traverse", "back", "forward", or "auto" ("auto" means everything except "reload").

Configuring the navigations

To configure a Velvette object to handle navigations, we need to provide routes, rules, captures, and styles. For example:

new Velvette({
    routes: {
        "home": "/",
        "about": "/about"
    },
    rules: [
        {to: "home", type: "back", class: "slide-right"}
    ],
    captures: {
        "section#main": "main.slow"
    },
    styles: {
        '::view-transition-group(".slow")': {animationDuration: "3s"}
    }
});

When responding to a navigation, Velvette would find the last matching rule in the rules list, and apply its class and parameters. If a matching rule is found, the view transition would be invoked and the specified class, captures and styles would be activated, exactly like calls to extend on a particular ViewTransition.

Transition between list and details

See issue #8209.

One thing that came up a lot from early adopters of CSS view-transitions is the difficulty to create transitions between list & details pages, e.g. a playlist in https://example.com/playlist/ that animates the song thumbnail to the hero in https://example.com/song/315 when selecting the appropriate song in the playlist.

In Velvette, this is done in the navigation configuration, like so:

new Velvette({
    routes: {
        "playlist": "/playlist/",
        "song": "/song/:song_id"
    },
    rules: [
        // "with" would match both song<->playlist and playlist<->song
        {with: ["song", "playlist"], class: "expand"}
    ],
    captures: {
        ".vt-expand.vt-route-song img#song-artwork": "artwork",
        ".vt-expand.vt-route-playlist ul.playlist li#song-$(song_id) img.thumbnail": "artwork"
    }
});

This example demonstrates several things that happen as a response to navigation:

  • The class vt-expand is applied for the entire duration of the transition.
  • The classes vt-route-song and vt-route-playlist are applied at the appropriate times only.
  • The second capture replaces the $(song_id) string with the value of the song_id parameter coming from either route (in this case, the song route).

Triggering the navigation

Once we have a configured Velvette object, we can apply it to navigations in 3 different ways.

Same-document (custom)

To potentially start a view transition for a same-document navigation, we simply call velvette.startNavigation, like so:

const velvette = new Velvette(config);
const transitionOrNull = velvette.startNavigation({
    from: "https://example.com/old-url",
    to: "https://example.com/new-url",
    // "push" | "replace" | "traverse" | "reload"
    navigationType,
    // e.g. -1 is "back"
    traverseDelta
}, async () => {
    // update the DOM to the new state
});

This allows integrating Velvette with routers or other styles of SPA authoring.

Same-document (with the Navigation API)

With the Navigation API, the information about old URL, new URL and navigation type is already known to us, so Velvette provides a convenient way to use CSS view-transitions together with it:

const velvette = new Velvette(config);
navigation.addEventListener("navigate", async event => {
    if (shouldIntercept(event)) {
        velvette.intercept(event, {
            async handler() {
                /* make actual changes based on the navigation */
            }
        });
    }
});

Cross-document

Note: cross-document navigations are currently only available in Chrome canary with experimental web features flag enabled, and is missing a few key features. Velvette already works with the current set of features, and aims to keep up with the changes until cross-document view transitions are stable.

To apply a Velvette configuration for cross-document view transitions:

// This has to be called very early, before the first render opportunity.
// e.g. in a classic script in the <head>.
new Velvette(config).crossDocument();