Enhance CSS view transitions with Velvette
Tags: webdev, css
Published: 2024-05-30 14:03:25

**Written by [David Omotayo](https://blog.logrocket.com/author/davidomotayo/)✏️**

Page transitions are pivotal in shaping user experience in modern web design and applications. Tools like CSS transitions and the Web Animations API help create visual cues for navigation and indicate navigation flow. 

Effective page transitions also help reduce cognitive load by helping users maintain context and perceive faster loading times. However, implementing these from scratch can be quite complex due to the CSS and JavaScript boilerplate code required, managing the state of elements, and ensuring accessibility when both states are present in the DOM. 

The CSS View Transitions API tackles most of these challenges, but can be difficult to work with for its own reasons — for example, the fact that it’s a novel API with diverse usage. This is where tools like Velvette come into play. 

Velvette is a library that simplifies view transition implementations and helps mitigate these challenges. In this article, we'll introduce Velvette, explore its features, and explain how to integrate it into existing projects.

## A quick primer on CSS View Transitions

[The CSS View Transitions API](https://blog.logrocket.com/getting-started-view-transitions-api/) introduces a way to smoothly change the DOM while simultaneously animating the interpolation between two unrelated states without any overlap between them. 

The underlying logic that makes this work is that the browser captures an element and takes two snapshots: one of the old state before the change and another of the new state after. To make this work, you need two parts. 

First, you need the `view-transition-name` property assigned to the element’s selector in your stylesheet:

```css
.element{
  view-timeline-name: transition-name;
}
```

Second, you need the method that updates the DOM wrapped in the `document.startViewTransition()` function:

```css
document.startViewTransition(() =>
  updateTheDOMSomehow();
);
```

This declaration instructs the browser to capture the snapshots, stack them unto each other, and create a transition using a fade animation.

## The challenges of the CSS View Transitions API

Developers who have worked with the View Transitions API since its release have likely encountered one or more of the following challenges:

*   **Unique name generation**: CSS View Transitions requires each element to have a unique name shared between its old and new states. Defining these names for multiple elements can become tedious quickly
*   **Scoped transitions**: View Transitions affects the entire document, which can lead to a lot of pointless captures on a page with multiple transitions
*   **Navigation handling**: Implementing transitions based on specific navigation patterns can be difficult and require significant boilerplate JavaScript code

Now, let’s see how Velvette can mitigate these challenges.

## Introducing Velvette

[Velvette](https://noamr.github.io/velvette/) is a utility library developed to make working with view transitions easier. It tackles issues like redundant boilerplates and monotonous generation of unique names, allowing developers to focus on crafting smooth animations. 

The library offers a declarative way to manage transition behavior in your application. You can define transitions for isolated elements — elements that operate independently — or in response to navigation events. These declarations are then seamlessly integrated with the View Transitions API. 

Velvette's key features include:

*   **Adding temporary classes**: Velvette dynamically adds temporary classes to the document element during the transition process. These classes serve as markers for capturing the different states of the transition. For example, when transitioning from a list view to a details view, Velvette might add a class like `morph` during the transition
*   **Constructing styles**: While the transition is animating, Velvette constructs additional styles. These styles define how the elements should appear during the transition. For instance, if you’re fading out a list view and fading in a details view, Velvette will assign necessary classes that handle opacity, animation timing changes, and other visual adjustments
*   **Assigning** `view-transition-name` **properties**: The `view-transition-name` property is crucial for specifying which elements participate in the transition. Velvette generates and sets these properties based on predefined rules. This ensures that the correct elements are animated during the transition

In the upcoming sections, we’ll see more about how Velvette works and how to get started with it in your next project.

## Velvette building blocks

Velvette provides two key functions: a `Velvette` constructor and a `startViewTransition` method. These functions offer simplified methods for extending view transitions in response to a DOM update, catering to specific requirements.

### The `startViewTransition` method

The `startViewTransition` method is ideally used for integrating straightforward transition animations, like sorting animations, to one or multiple elements on a page. It eliminates the need to manually declare transition names and avoids unnecessary captures. 

The method accepts an object containing configuration options as its arguments:

```css
startViewTransition({
  update: () => {...},
  captures: {...},
  classes: ...,
});
```

Here is a breakdown of the argument object:

*   `update`: This is a callback function that defines how the DOM will be updated during the transition. For example, if you have a separate function named `updateTheDOM` that handles DOM manipulation, you would pass that function as the update argument
*   `captures`: This object allows you to define elements to be captured for the transition. It uses a key-value structure. Keys are selectors for the elements you want to capture (e.g., class names, IDs), and the values define how to generate unique view-transition-name properties (often using element IDs or other unique identifiers)
*   `classes`: This is an optional array of CSS class names that will be temporarily added to the document element during the transition. Adding these classes can be useful for applying specific styles during the animation

### The `Velvette` constructor

The `Velvette` constructor is designed for creating complex view transition animations across page navigation. A typical example is a smooth image transition — like expanding or shrinking — when a user navigates between a list and a detail page. 

Similar to `startViewTransition`, the constructor accepts a config object with various options as its argument:

```css
const velvette = new Velvette({
  routes: {
      details: ""..."
      list: "..."
  },
  rules: [{
      with: ["list", "details"], class: ...
  }, ],
  captures: {
    ...
  }
});
```

Here is a breakdown of the config options:

*   `routes`: This object defines named routes for different views in your application. It uses key-value pairs, where the keys are route names and the values can be URLs that uniquely identify the view
*   `rules`: This is an array of rules that match specific navigation patterns. Each rule defines which navigations trigger a view transition and specifies the class and parameter to associate with the transition
*   `captures`: Similar to the `startViewTransition` method, this option allows you to define elements to capture during navigation transitions. This provides more granular control over the elements involved in the animation

## Installing and setting up Velvette

Velvette is built as an add-on, which means we can add it to existing projects by simply including the following script tag into the `index.html` file:

```html
><script src="https://www.unpkg.com/velvette@0.1.10-pre/dist/browser/velvette.js"></script>
```

We can also add it with npm using the following command:

```bash
>npm install velvette
```

## Integrating Velvette with existing projects

Once Velvette is integrated into your project, you can start using the library by importing the `startViewTransition` or `Velvette` constructor in the needed components or pages:

```css
import {Velvette, startViewTransition} from "velvette";
```

Alternatively, if you've included Velvette using a CDN link, you can simply call the Velvette constructor like so:

```css
const velvette = new Velvette({...});
```

This is possible because the CDN link automatically injects a global `Velvette` class directly onto your window object, which can be accessible across the document.

### List item animation

Now that you’ve successfully added Velvette to your project, you can replace every vanilla View Transitions implementation in your project with Velvette's. 

For example, let’s say you have a to-do application with a base View Transitions implementation like in the following example:

```css
document.addEventListener("DOMContentLoaded", () => {
const items = document.querySelectorAll(".item");
  items.forEach((item, index) => {
    item.id = `item-${index}`;
    item.addEventListener("click", (e) => {
     document.startViewTransition(() => moveItem(e));
    });
  });
});

const moveItem = (e) => {
  const item = e.target;
  var targetBoxId = item.closest(".box").id === "box1" ? "box2" : "box1";
  var targetList = document.getElementById(targetBoxId).querySelector("ul");

  item.parentNode.removeChild(item);
  targetList.appendChild(item);
};
```

This base implementation would look like so:

See the Pen [Todo list transition](https://codepen.io/david4473/pen/wvZXJpM) by david omotayo ([@david4473](https://codepen.io/david4473)) on [CodePen](https://codepen.io).

In such a case, we can replace the `document.startViewTransition()` declaration:

```css
document.startViewTransition(() => moveItem(e)); 
```

With a Velvette declaration as follows:

```css
>Velvette.startViewTransition({
    update: () => moveItem(e)
});
```

This will invoke Velvette, call the `moveItem()` function on every item click, and apply the default fade animation to each item on the list when they are removed or appended to either the `Tasks` or `Completed Tasks` parent elements. 

However, for each item to animate smoothly, it needs a unique `view-transition-name` value. 

Let's suppose we assign a transition name only to the first item on the list :

```css
#item-0{
  view-transition-name: item;
}
```

As expected, only the first item animates: ![Demo To Do List Showing User Clicking Items To Move From Tasks To Completed Columns, But With Animation Only Applying To First List Item](https://blog.logrocket.com/wp-content/uploads/2024/05/Demo-to-do-list-only-first-item-animates.gif) To achieve the same effect for all the items, we'd traditionally need to assign a unique `view-transition-name` value to each one, which can be quite tedious. This is where Velvette's `captures` object comes in. Instead of manual assignment, you can leverage captures to dynamically map between item selectors and assign temporary `view-transition-name` values during the transition:

```css
Velvette.startViewTransition({
        update: () => moveItem(e),
        captures: {
          "ul#list li[:id]": "$(id)",
        },
      });
```

Here, we capture every child `li` element within the `#list` selector and use the element's `id` to generate a `view-transition-name` property. 

This may seem a bit overwhelming, so let's break it down. Remember, an `id` is assigned to each item on the list:

```css
const items = document.querySelectorAll(".item");
  items.forEach((item, index) => {
    item.id = `item-${index}`;
        ...
  });
```

And their parent elements are assigned a `list` ID selector:

```html
<div>
        <h2>Tasks</h2>
        <ul id="list">
                ...
        </ul>
      </div>
        <div>
        <h2>Completed Tasks</h2>
        <ul id="list">
                ...
        </ul>
      </div>
```

The `captures` object looks for the `ul` element with the `list` class selectors in the code above, maps through its `li` child elements, grabs the ID we assigned in the previous code, and assigns it to their `view-transition-name` declarations:

```css
captures: {
  "ul#list li[:id]": "$(id)",
},
```

The `view-transition-name` declaration for each item on the list will look something like this:

```css
>#item-0{
  view-transition-name: item-0;
}
#item-1{
  view-transition-name: item-1;
}
#item-2{
  view-transition-name: item-2;
}
...
```

And the result: ![Updated To Do List With Items Correctly Animating When Moving From Column To Column](https://blog.logrocket.com/wp-content/uploads/2024/05/Updated-to-do-list-items-correctly-animating.gif) As you can see, the animation now works correctly for every list item.

### Navigation animation

A common use case for the View Transitions API is handling animations during page navigation, essentially transitioning between the outgoing and incoming pages. As mentioned before, a popular example involves animating the navigation between a list view and a details page: ![Demo Transition Effect When Navigating Between A List View And A Details Page](https://blog.logrocket.com/wp-content/uploads/2024/05/Transition-effect-navigating-list-view-details-page.gif) Implementing this transition effect from scratch can be challenging. It typically involves triggering a view transition when navigating between the list and details pages. 

One way to achieve this is by intercepting the navigation event and encapsulating the DOM update function — the function that modifies the page content — within the View Transitions API's `startViewTransition` method. 

Here's an example:

```css
async function init() {
  const data = await fetch("products.json");
  const results = await data.json();

  function render() {
    const title = document.getElementById("title");
    const product_list = document.querySelector("#product-list ul");
    product_list.innerHTML = "";

    for (const product of results) {
      const li = document.createElement("li");
      li.id = `product-${product.id}`;
      li.innerHTML = `
      <a href="?product=${product.id}">
      <img class="product-img" src="${product.image}" />
      <span class="title">${product.title}</span>
    </a>
      `;
      product_list.append(li);
    }

    const searchParams = new URL(location.href).searchParams;
    if (searchParams.has("product")) {
      const productId = +searchParams.get("product");
      const product = results.find((product) => product.id === productId);
      if (product) {
        const details = document.querySelector("#product-details");
        details.querySelector(".title").innerText = product.title;
        details.querySelector("img").src = `${product.image}`;
      }
    }

    if (searchParams.has("product")) {
      title.innerText = "Product Details";
    } else {
      title.innerText = "Product List";
    }

    document.documentElement.classList.toggle(
      "details",
      searchParams.has("product")
    );
  }

  render();

 navigation.addEventListener("navigate", (e) => {
    e.intercept({
      handler() {
        document.startViewTransition(() => {
          render();
        });
      },
    });
  });
}

init();
```

In this code example, we used [the Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) to intercept navigation between a list and details page and trigger a view transition that is applied to `render()` function: ![Using The Navigation Api To Intercept Navigation Between Pages And Trigger A View Transition](https://blog.logrocket.com/wp-content/uploads/2024/05/Using-Navigation-API-intercept-navigation-pages-trigger-view-transition.gif) You can find the complete code for this example in [this GitHub repository](https://github.com/david4473/Velvette-navigation-transition-example). 

Note that the Navigation API currently has [limited browser support](https://caniuse.com/mdn-api_navigation) — it’s only available on Chromium-based browsers. To ensure good UX for a wider range of users, consider implementing fallback mechanisms for unsupported browsers. 

This basic implementation provides a starting point, but achieving a more complex effect requires additional steps. 

For example, to morph thumbnails between the list and details pages, we would have to assign identical `view-transition-name` values to the corresponding thumbnails on both the details and list pages. However, this assignment needs to be done strategically:

*   It shouldn't happen simultaneously to avoid skipping the transition
*   It should be assigned to the specific item involved in the transition on the list

While the following code snippet might require some adjustments, it demonstrates the core concept:

```css
.details-thumbnail {
  view-transition-name: morph;
}

 //Javascript

list-thumbnail.addEventListener(“click”, async () => {
  list-thumbnail.style.viewTransitionName = "morph";

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = "";
    updateTheDOMSomehow();
  });
};
```

The biggest drawbacks of depending on this method are the unnecessary complexity it introduces and the boilerplate code it adds to your project. 

Velvette simplifies this process by offering a centralized configuration system. This configuration handles the heavy lifting behind the scenes, eliminating the need for manual implementation and saving you time and effort:

```css
const velvette = new Velvette({
    routes: {
      details: "?product=:product_id",
      list: "?products",
    },
    rules: [
      {
        with: ["list", "details"],
        class: "morph",
      },
    ],
    captures: {
      ":root.vt-morph.vt-route-details #details-img": "morph-img",
      ":root.vt-morph.vt-route-list #product-$(product_id) img": "morph-img",
    },
  });
```

This Velvette configuration is a replica of the navigation transition we tried to implement manually earlier using view transition. 

In this configuration, we use the `routes` and `rules` properties to define which navigation triggers a view transition. In this case, any navigation between the `list` and `details` routes will initiate a view transition and add a `morph` class to the transition:

```css
routes: {
      details: "?product=:product_id",
      list: "?products",
    },
    rules: [
      {
        with: ["list", "details"],
        class: "morph",
      },
    ],
        ...
```

The `captures` property tackles the previously mentioned challenge of assigning unique `view-transition-name` properties during transitions:

```css
captures: {
      ":root.vt-morph.vt-route-details #details-img": "morph-img",
      ":root.vt-morph.vt-route-list #product-$(product_id) img": "morph-img",
    },
```

Here, we use a key-value pair of selectors and values to assign identical transition names, `morph-img`, to generate a `view-transition-name` for both the details page thumbnail and the clicked product item image. 

The `":root.vt-morph.vt-route-details #details-img"` selector is a combination of:

*   The transition class — `vt-morph` from the rules object
*   The route where we want to capture the `morph` transition — `vt-route-details`
*   The image's selector — `#details-img`

Note that the `vt` prefix is required for Velvette to recognize the selectors. 

The second selector, `":root.vt-morph.vt-route-list #product-$(product_id) img"`, uses the same method to add the `morph-img` transition name to the selected product item during the `morph` transition. The only difference is that it applies only when in the `list` route and the `${product_id}` expression will be replaced by the product item's ID, like so:

```css
:root.vt-morph.vt-route-list  #product-1 img: ...,
```

Finally, we can leverage Velvette to intercept navigation and apply the configurations defined above. To achieve this, we'll update the previous navigation declaration as follows:

```css
navigation.addEventListener("navigate", (e) => {
    velvette.intercept(e, {
      handler() {
        render();
      },
    });
  });
```

Here’s the result: ![Updated Navigation Declaration With Velvette Showing Smoother Transitions Between The Product Listing And Details Pages](https://blog.logrocket.com/wp-content/uploads/2024/05/Updated-navigation-declaration-Velvette.gif)

## Conclusion

In this article, we introduced Velvette and explored its building blocks and how they work to achieve smoother and more engaging transitions between views. We also explored how to integrate Velvette into existing projects without a total overhaul of your existing code. 

While Velvette offers powerful transition capabilities, it's built on the View Transitions API, which currently has limited browser support, so consider implementing fallback mechanisms for browsers that don't support the API. 

We've only scratched the surface of what you can achieve with Velvette in this article. If you're eager to learn more about the library, the [Velvette documentation](https://noamr.github.io/velvette/) offers comprehensive examples that will assist you in getting started.

---

##Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, [try LogRocket](https://lp.logrocket.com/blg/css-signup).

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2019/12/cpu-memory-usage.png)](https://lp.logrocket.com/blg/css-signup)

[LogRocket](https://lp.logrocket.com/blg/css-signup) is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — [start monitoring for free](https://lp.logrocket.com/blg/css-signup).