A Sneak Peek at the View Transition API

Photo by Kent Tupas on Unsplash

A Sneak Peek at the View Transition API

Pre-requisite

  • Basic understanding of HTML, CSS, and Javascript.

  • Good grasp of the Document Object Model (DOM).

  • An Updated Browser, preferably Chrome.

Introduction

Creating smooth transitions on the web is a complex task, and calling it challenging is an understatement. After years of fine-tuning to discover the ideal library or tool to alleviate this pain, we can now effortlessly produce straightforward yet stylish animations with just a few lines of code.

At the core of this revolution is the ViewTransitionsAPI, designed to simplify polished transitions by capturing a snapshot of the page. This API allows the Document Object Model (DOM) to seamlessly transition from one state to another by strategically overlapping states.

In this article, you will explore the challenges associated with creating animations on the web. You'll witness firsthand how the ViewTransitionsAPI transforms the process, reducing the complexity to just a few lines of code that you can begin working with immediately.

Additionally, you'll have the opportunity to craft a mock landing page for a blog. By utilizing the ViewTransitionAPI, you will effortlessly incorporate captivating animations into the landing page.

Let's dive right in.

Goals

After reading this article, you will be able to:

  • Gain insight into why the ViewTransitionsAPI is necessary for web development.

  • Understand the mechanics behind how the ViewTransitionsAPI operates.

  • Learn how to create straightforward animations using the capabilities of the ViewTransitionsAPI.

The Problem With Transitions On the Web

Although animating individual components on a web page has brought joy, transitioning between web pages and different DOM states on the same page has proven challenging. However, in recent years, the web developer community has witnessed a breakthrough in web animations and page transitions.

In this context, page transitions involve animating the switch between pages and different DOM states within the same page. Unlike mobile applications, where page transitioning is smooth and provides users with contextual navigation cues, the web often replaces the older page instantly without a transition.

The challenge stems from the fact that web transitions require synchronizing parts of the state of the old page with parts of the state of the new page. However, on the web, the old page disappears before the new page arrives, making synchronization impossible.

Unfortunately, animating different DOM states of the same document proves to be very complicated with the current APIs, let alone animating page transitions.

Even something as seemingly simple as a fade becomes challenging when confronted with the task of maintaining the states of both the old and new pages throughout the transition. This difficulty is compounded by current frameworks and the accessibility and usability pitfalls inherent in web development.

Scenarios such as transitioning from a scroll page to a static page or ensuring that states of pages containing videos or iframes don't reset while navigating around the website are commonplace challenges.

To address these issues, one might consider temporarily moving the item to the root of the page, requiring substantial changes to the CSS. While not impossible, it is undeniably difficult.

In contrast, mobile technologies like Android and iOS boast APIs that effortlessly handle such challenges.

To level the playing field, extensive experimentation has led to numerous iterations attempting to solve these problems. A recent and increasingly stable solution gaining popularity is the ViewTransitionAPI, our focal point for today.

View Transitions API to the rescue!

The ViewTransitionAPI originated as an idea proposed by the Google web team and is now gaining traction for adoption in various technologies and browsers.

While existing APIs like the CSStransition API and WebAnimationAPI already cater to browser animations, the ViewTransitionAPI stands out as a remarkable addition.

Its uniqueness lies not only in its simplicity but also in its ability to capture two distinct DOM states and seamlessly coordinate their transition—an innovation that sets it apart.

The primary goal of the ViewTransitionAPI is to provide access to both the old and new states before and after a DOM change.

It addresses challenges such as managing the loading and positioning of the old and new content, preventing the old page content from interfering with the new content, and appropriately removing the old content once the transition is successfully achieved.

Syntax and Mechanism

One notable feature of the ViewTransitionAPI lies in the simplicity of its syntax. Achieving easy animations with just a few lines of JavaScript is no small accomplishment.

NOTE: The snippet below illustrates the fundamental usage of the ViewTransitionAPI. It is presented in pseudocode for explanatory purposes and does not produce any actual results. The syntax would be implemented to demonstrate results in the actual demo.

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOM(data);
    return;
  }
  // With a transition:
  document.startViewTransition(() => updateTheDOM(data));
}
  • The function spaNavigate(data) incorporates an if statement to offer an alternative method of updating the DOM—calling updateTheDOM(data)—in case the browser does not support the ViewTransitionAPI.

  • Conversely, if the ViewTransitionAPI is supported, the function utilizes a built-in method to update the DOM. This is achieved with the following line: document.startViewTransition(() => updateTheDOM(data));.

  • The mechanism behind this is that when the ViewTransitionAPI is invoked using the method document.startViewTransition(() => updateTheDOM(data));, it captures the current state of the page.

  • The method updateTheDOM is presented in pseudocode and would be substituted in a real-world context. Whatever replaces it will be the animation used for the transition.

  • Once the animation is complete, the ViewTransitionAPI updates the DOM by altering the state while retaining the old state. Subsequently, the API captures the new state of the page and executes a transition between both states, avoiding abrupt state changes.

  • In our upcoming demo, the remarkable aspect to observe is that the ViewTransitionAPI takes care of all the essential tasks—capturing old and new states, managing overlap—using just a few lines of code.

Pseudo Classes

Among the numerous intricacies of the ViewTransitionAPI, a key feature is its provision of pseudo-classes that facilitate the customization of transitions in your CSS file when transitioning from one page to another.

These pseudo-classes group DOM elements in a way that allows the API to manage the transition states. As a result, you can opt to control the animation at various stages of the DOM change, apply distinct animations to different sections of the DOM, or manage the entire page's animation.

While these pseudo-classes serve as entry points for creating custom animations, today's article focuses on understanding how the ViewTransitionAPI operates in the browser through JavaScript.

Nonetheless, having a grasp of these pseudo-classes can be beneficial:

  • ::view-transition : Represents the root of the view transitions overlay, which contains all view transitions and sits above other page content. Used for styling the overall transition container.

  • ::view-transition-old: Represents the "old" view state before the transition (a static screenshot). Used to style the old view's appearance during the transition.

  • ::view-transition-new: Represents the "new" view state after the transition (a live representation). Used to style the new view's appearance during the transition.

  • ::view-transition-image-pair: Represents a pair of old and new views within a transition group. Used to style the visual relationship between the transitioning views.

Project Support

On the example provided by the Mozilla Developer Network, a practical demonstration using pure HTML showcases a seamless crossfade transition from one image to another. This feature, being an actual part of HTML, implies that any JavaScript library or framework can integrate it into their technology.

As of now, the stable support for browsers includes this version of the ViewTransitionAPI, offering convenient access to DOM transition changes. Some frameworks have adopted the API using polyfills to create page transitions for Multi-Page Apps (MPAs). Therefore, as of the time of this article's writing, using the ViewTransitionAPI in JavaScript is the most easily accessible browser-stable version.

Several major technologies have embraced and incorporated the ViewTransitionAPI for page transitions, including AstroJS, AngularJS, Svelte/SvelteKit, HTMX, and Nuxt.

It's important to note that any JavaScript script can invoke this API in a supporting browser, but certain frameworks and libraries may provide better support than others.

Demo

In today's demonstration, we will construct a mock blog landing page featuring several text options. The content will be dynamically sorted by clicking on designated buttons using HTML, CSS, and JavaScript.

Once the project setup is complete, we'll have the capability to easily organize and navigate through the article titles, as demonstrated below:

Next, we will leverage the power of the ViewTransitionAPI to introduce basic animations seamlessly during the sorting process. This is a notable enhancement, considering that achieving such animations would typically require an extensive amount of CSS and scripting.

While the demonstration is relatively straightforward, the focus is on showcasing the effectiveness of the ViewTransitionAPI in achieving these animations.

Setting Up

To keep things efficient and dive into the main part swiftly, I'll share the code along with a quick overview of its workings.

  1. Create three files.
viewBlog.html
viewBlog.css
viewBlog.js
  • As usual, we have three files for the structure of our HTML, CSS for styling and Javascript for some custom behaviour.
  1. In our HTML file, put the code below:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>View Transitions</title>
    <link rel="stylesheet" href="viewBlog.css" />
  </head>
  <body>
    <div class="content-grid">
      <header class="primary-header full-width">
        <div class="primary-header__layout breakout-right">
          <nav class="primary-navigation">
            <ul>
              <li><a href="#">About Open Replay</a></li>
              <li><a href="#">Tutorials</a></li>
              <li><a href="#">Product</a></li>
              <li><a aria-current="page" href="#">Write for us</a></li>
            </ul>
          </nav>

          <div class="account-links">
            <a href="#">Log In</a>
            <a href="#" class="create-account">Get Started</a>
          </div>
        </div>
      </header>
      <nav aria-label="Breadcrumb" class="breadcrumbs">
        <ol>
          <li><a href="#">Product</a></li>
          <li><a href="#" aria-current="page">Blog</a></li>
        </ol>
      </nav>
      <main class="main-with-aside">
        <div class="flow">
          <h1>Our most read articles</h1>
          <div class="filter">
            <h2 class="visually-hidden">Filter by type of article</h2>
            <button class="filter-btn active" data-filter="all">All</button>
            <button class="filter-btn" data-filter="frontend">Frontend</button>
            <button class="filter-btn" data-filter="backend">Backend</button>
            <button class="filter-btn" data-filter="fullstack">
              Fullstack
            </button>
          </div>

          <ul class="article-list">
            <li class="article" data-category="fullstack">
              <div class="article-details">
                <span class="article-date">Published on : Jan 29th 2023</span>
                <span class="article-category">Fullstack</span>
              </div>
              <h2 class="article-name">Building a blog with Next JS</h2>
            </li>

            <li class="article" data-category="fullstack">
              <div class="article-details">
                <span class="article-date">Published on : March 1st 2023</span>
                <span class="article-category">Fullstack</span>
              </div>
              <h2 class="article-name">
                Setting up React with a Vector Database
              </h2>
            </li>

            <li class="article" data-category="backend">
              <div class="article-details">
                <span class="article-date"
                  >Published on : August 23rd 2023</span
                >
                <span class="article-category">Backend</span>
              </div>
              <h2 class="article-name">NodeJS Security Best Practices</h2>
            </li>

            <li class="article" data-category="frontend">
              <div class="article-details">
                <span class="article-date">Published on : June 7th 2023</span>
                <span class="article-category">Frontend</span>
              </div>
              <h2 class="article-name">CSS trigonometry</h2>
            </li>

            <li class="article" data-category="frontend">
              <div class="article-details">
                <span class="article-date">Published on : September 14th</span>
                <span class="article-category">Frontend</span>
              </div>
              <h2 class="article-name">View Transitions API</h2>
            </li>
          </ul>
        </div>
      </main>
    </div>
    <script src="viewBlog.js"></script>
  </body>
</html>
  • This code comprises markup for our simulated webpage, linked to both our CSS and JavaScript files, which will be addressed shortly.

  • It's essential to observe that for sorting the text content, JavaScript will be employed. Consequently, the HTML structure incorporates data- attributes such as data-filter and data-category to facilitate straightforward sorting with JavaScript.

  • Here's a glimpse of the raw structure of the page:

  1. On to viewBlog.css for styling.
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.7;
}

.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

.flow > * + * {
  margin-block-start: var(--flow-spacing, 1rem);
}

.content-grid {
  --container-max-width: 60rem;
  --min-inline-margin: 2rem;
  display: grid;
  grid-template-columns:
    [full-width-start breakout-left-start] minmax(1rem, 1fr)
    [content-start breakout-right-start] min(
      100% - var(--min-inline-margin),
      var(--container-max-width)
    )
    [content-end breakout-left-start] minmax(1rem, 1fr)
    [full-width-end breakout-right-end];
}

.content-grid > *,
.full-width > * {
  grid-column: content;
}

.content-grid > .full-width {
  grid-column: full-width;

  display: grid;
  grid-template-columns: inherit;
}

.breakout-right {
  grid-column: breakout-right;
}

.breakout-left {
  grid-column: breakout-left;
}

.primary-header {
  margin-block-end: 3rem;
  border-block-end: 1px solid var(--separator-color, black);
}

.primary-header__layout {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;

  /*   grid-column: content-start / full-width-end; */
}

.primary-navigation > ul {
  list-style: none;
  margin: 0;
  padding: var(--navigation-padding, 1rem) 0;
  display: flex;
  gap: clamp(1rem, 5vi, 2.5rem);
}

:is(.primary-navigation, .account-links) :where(a) {
  font-weight: 500;
  text-decoration: none;
  color: var(--link-color, #777);
}

:is(.primary-navigation, .account-links)
  :where(a:hover, a:focus, a[aria-current="page"]) {
  color: var(--link-current-color, #111);
}

.account-links {
  display: flex;
}

.account-links > * {
  display: flex;
  align-items: center;
  padding-block: var(--navigation-padding, 1rem);
  padding-inline: 3rem;
}

.create-account {
  background: var(--button-bg, #333);
  color: var(--button-text, white);
}

.main-with-aside {
  --flow-spacing: 2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 3rem;
}

.main-with-aside > :not(aside) {
  flex-basis: 55ch;
  flex-grow: 999;
}

.main-with-aside > aside {
  flex-basis: 325px;
  flex-grow: 1;
}

.breadcrumbs ol {
  list-style: none;
  padding: 0;
  display: flex;
  gap: 0.5rem;
}

.breadcrumbs a {
  display: inline-block;
  padding: 0.25rem 1rem;
  border-radius: 100vw;
  background: var(--breadcrumb-color, #eee);
  color: inherit;
  font-weight: 500;
  text-decoration: none;
}

.breadcrumbs li:not(:last-child)::after {
  content: " / ";
  display: inline-block;
  font-size: 1.75em;
  font-weight: 100;
  transform: skew(-20deg);
  line-height: 0;
  margin-inline: 0.5em 0;
}

.breadcrumbs a:hover,
.breadcrumbs a:focus {
  background: #ccc;
}

.article-list {
  /* display: grid; */
  list-style: none;
  padding: 0;
}

.article:not(:last-child):not(:has(+ [hidden])) {
  padding-bottom: 2rem;
  margin-bottom: 2rem;
  border-bottom: 1px solid black;
}

.article-date::after {
  content: " •";
}

.filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  border: 0;
  padding: 0;
}

.filter-btn {
  cursor: pointer;
  text-transform: uppercase;
  background: transparent;
  border: 0;
  border-radius: 0.25rem;
  padding: 0.25rem 1rem;
}

.filter-btn:hover,
.filter-btn:focus {
  background-color: #ccc;
}

.filter-btn.active {
  background: black;
  color: white;
  box-shadow: 0 0 0.5rem rgb(0 0 0 / 0.2);
}
  • This gives us a visually appealing page with distinct headings and navigation buttons, although currently non-functional.

  • At this point, the navigation buttons remain inactive, providing us with a static page illustrated below:

  1. Next up, our aim is to develop a script enabling seamless navigation between various article categories. Upon clicking on a navigation bar option such as "All," "Fullstack," and others, the script will dynamically update the display to show only articles associated with the selected tag.
const filterList = document.querySelector(".filter");
const filterButtons = filterList.querySelectorAll(".filter-btn");
const articles = document.querySelectorAll(".article");

filterButtons.forEach((button) => {
  button.addEventListener("click", (e) => {
    const filter = e.target.getAttribute("data-filter");
    updateActiveButton(e.target);
    filterArticles(filter);
  });
});

function updateActiveButton(newButton) {
  filterList.querySelector(".active").classList.remove("active");
  newButton.classList.add("active");
}

function filterArticles(articleFilter) {
  articles.forEach((article) => {
    const articleCategory = article.getAttribute("data-category");
    if (articleFilter === "all" || articleFilter === articleCategory) {
      article.removeAttribute("hidden");
    } else {
      article.setAttribute("hidden", "");
    }
  });
}
  • First, we begin by selecting DOM elements using their respective class names.

  • When a filter button is clicked, the filter category is extracted from the button's data-filter attribute. Subsequently, two functions are invoked: updateActiveButton visually highlights the active button, while filterArticles filters articles based on the selected category.

  • The updateActiveButton function accepts a button as a parameter. It identifies the currently active button and modifies its visual state by adding or removing the active class.

  • Meanwhile, the filterArticles function takes a filter category as a parameter. It iterates through the articles, showing or hiding them based on whether they match the selected category. If the category is "all," it displays all articles.

  • It's worth noting that the process allows for seamless switching based on the clicked option. This transition occurs instantly, altering the DOM contents without any animations.

Applying Animations

Our aim now is to infuse a modern touch into the webpage and provide users with a more dynamic experience. Thanks to the ViewTransitionsAPI, achieving this is a breeze. Take note of how I leverage it to introduce animations.

  1. Begin by instructing the browser to animate transitions when switching between options. This means that when articles are rendered based on filters, a smooth animation will accompany the transition. Implement the following change within the filterButtons loop to enable this animation:
filterButtons.forEach((button) => {
  button.addEventListener("click", (e) => {
    const filter = e.target.getAttribute("data-filter");
    // New Function
    document.startViewTransition(() => {
      updateActiveButton(e.target);
      filterArticles(filter);
    });
  });
});
  • To facilitate the animation of the transition, I introduced a new method provided by the ViewTransitionsAPI called document.startViewTransition.

  • Subsequently, I encapsulated the filtering functions within the document.startViewTransition. This ensures that whenever the page initiates a filter, it undergoes an animated transition first. Isn't it exhilarating?

  • Take a moment to observe the image below. It vividly illustrates how the transition unfolds when filtering articles—gradually animated through cross-fading, the primary animation feature offered by the ViewTransitionsAPI:

  1. Nonetheless, it's essential to acknowledge that the ViewTransitionsAPI might not be universally supported across all browsers. Hence, a fallback mechanism is necessary. In our filterButtons loop, we will incorporate a fallback solution to ensure that if the API is not supported, the elements will undergo an instant change without any animation.
filterButtons.forEach((button) => {
  button.addEventListener("click", (e) => {
    const filter = e.target.getAttribute("data-filter");
    // New IF statement
    if (!document.startViewTransition) {
      updateActiveButton(e.target);
      filterArticles(filter);
    }
    document.startViewTransition(() => {
      updateActiveButton(e.target);
      filterArticles(filter);
    });
  });
});
  • With our added fallback, if (!document.startViewTransition), the browser handles scenarios where the ViewTransitionsAPI is not supported. In such cases, the filtering through articles proceeds without animation, ensuring a seamless experience even in the absence of API support.

As mentioned earlier, the ViewTransitionsAPI comes with pseudo-classes that facilitate the animation of DOM states at various stages, enabling the animation of the entire webpage or specific sections of the DOM.

Suppose we wish to style the lines and elements separating the articles on the demo page. This can be achieved using either Javascript or CSS by assigning a view transition name to the relevant elements.

Opting for Javascript is particularly advantageous because it allows us to dynamically generate unique view transition names for each element we intend to animate. This approach simplifies the process of creating and applying view transitions to animate these elements effectively.

  1. To do animate specific elements, make these changes to viewBlog.js.
const filterList = document.querySelector(".filter");
const filterButtons = filterList.querySelectorAll(".filter-btn");
const articles = document.querySelectorAll(".article");

let articleIndex = 0; // New Line

// New Loop
articles.forEach((article) => {
  article.style.viewTransitionName = `title-${++articleIndex}`;
});
  • After the part of your code where you make DOM declarations, add the next two pieces of code.

  • What those two lines, specifically the new loop, articles.forEach, do is to create a view transition name for every DOM element with the class article. Then, instead of only cross-fading the entire page, it will also cross-fade the specific elements.

  • The idea here is that to create a basic animation for specific elements, all you have to do is give them a view transition name. Now, isn't that wonderful?

  • In the image below, notice how the break lines and article titles are sliding up and down when you change the filter option:

Browser Support

The ViewTransitionsAPI is currently shipped in two modes. The first is the JavaScript API, which can be used directly or integrated into various frameworks. The second mode allows for direct use in HTML, though it is considered crude and isn't publicly available at the time of writing this article.

Major browsers that currently support the direct use of ViewTransitionsAPI include Chrome, Edge, and Opera.

It's important to note that many features of this API are still experimental. Therefore, I strongly recommend reviewing the browser support documentation before implementation.

Conclusion

This article is just a beginning in exploring a vast field, akin to taking a stab at a tree trunk. Trust me, this tree holds all the nectar you need for web animations, but it will take time to fully appreciate its richness.

As of the time of writing this piece, some features are still primarily experimental, with support limited to a few browsers at most.

However, don't let this discourage you from experimenting with what promises to be a significant advancement in the web animations landscape. Exploring the ViewTransitionsAPI might take some time before a deep dive is possible, but with this article, you've gained a substantial understanding of what it is and how it can be applied.

Continue to explore and stay tuned to this blog for more comprehensive updates on this topic. Until then, to all my animated enthusiasts, cheers!

Resources