Delightful UI Animations With Shared Element Transitions API (Part 2)

Delightful UI Animations With Shared Element Transitions API (Part 2)

In the first part of this article, we covered Shared Element Transitions API (SET API) and how we can use it to effortlessly create complex transitions for various UI elements, which would usually require a lot of JavaScript code or an animation library to achieve.

But what about smooth and delightful transition animations between individual pages? This is probably one of the most often requested features in the past few years because even with all the frameworks like React and Svelte and animation libraries like GSAP and Framer Motion, transitions between pages are still really difficult to do.

In this article, we’re going to showcase same-document page transitions commonly found in Single Page Applications and talk about the future of the Shared Element Transitions API for cross-document (Multi Page Application) transitions. I’ll also showcase some awesome React, Astro, and Svelte implementation examples from the dev community.

Note: Shared Element Transitions API is currently supported only in Chrome version 104+ and Canary with the document-transition flag enabled. Examples will be accompanied by a video, so you can easily follow along with the article if you don’t have the required browser installed.

In case you haven’t checked out my previous article on the topic, here is a quick rundown of this exciting new API so you can follow along with the article.

Shared Element Transitions API

With Shared Element Transitions API, the browser does a lot of heavy lifting when it comes to animations allowing us to create complex UI animations in a more streamlined way. The main part of the API is a JavaScript function that takes screenshots of the UI state before and after the DOM update and apples a crossfade animation:

const moveTransition = document.createDocumentTransition();
await moveTransition.start(() => {
  /* Take screenshot of an outgoing state */
  /* Update the DOM - move item from one container to another */
  targetContainer.append(activeItem);
  /* Take screenshot of an incoming state and crossfade the states */
});

Just by calling the start function, we get a neat and simple crossfade animation between the outgoing and incoming states.

As you can see, we can still navigate between the pages; DOM is updated with the new content, and the URL in the browser changes. We are intercepting the browser’s default navigation behavior and handling the page loading and DOM updates all by ourselves while we remain on the same page.

By just passing the DOM update function as a callback to the SET API start function, we get a neat crossfade transition between pages right out of the box!

With just a few lines of CSS and JavaScript, we’ve created this beautiful transition animation. All we had to do was to identify the shared element (item image) on a clicked link using a page-transition-tag and signal the browser to keep track of its dimension and position.

We get a crossfade animation on a shared element on backward navigation for free because the selector we used document.querySelector(a[href="${url.pathname}"] .card__image) runs on the current page, so when we navigate back to items list page the tag doesn’t get applied and browser cannot match the shared element.

If we want to have the same animation on the shared element when navigating back to the item list page, we have to apply the tag to the correct image element in the grid after we fetch the contents of a target page.

Customizing Page-Transition Animation With CSS

Let’s use CSS animation properties to fine-tune the crossfade and item image animation. We want the crossfade animation to be quick and more subtle, and the more elaborate image animation to be more noticeable and have a nice custom easing function:

/* Speed up crossfade animations */
::page-transition-outgoing-image(*),
::page-transition-incoming-image(*) {
    animation-timing-function: ease-in-out;
    animation-duration: 0.25s;
}

/* Fine-tune shared element position and dimension animation */
::page-transition-container(product-image) {
    animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    animation-duration: 0.5s;
}

We also need to keep in mind that some users might prefer browsing the site without the complex animations with a lot of movement, so we want to either turn them off or provide more appropriate animation:

@media (prefers-reduced-motion) {
  ::page-transition-container(*),
  ::page-transition-outgoing-image(*),
  ::page-transition-incoming-image(*) {
    /* Or add appropriate animation alternatives */
    animation: none !important; 
  }
}

Crossfade animations now run faster, and the sizing and position animation runs a bit slower and with a different timing function.

In this example, I’ve only showcased code snippets relevant to creating page transition and SET API. If you are curious about the complete source code or want to check the demo in detail, feel free to check out the project repository and inspect the demo page.

Upcoming Cross-document Transitions

Proper Shared Element Transitions API support for MPAs is still a work in progress, but we can get a general idea of how it’s supposed to work from a rough draft by WICG.

In same-document transitions, we would use pageTransition.start(/* … */) function to let the browser keep track of the DOM updates. As for the cross-document transitions, we need to run the transition request function on the outgoing page before it’s unloaded and run the transition on the incoming page once it’s ready.

The following code snippets are copied from the WICG draft:

// In the outgoing page
document.addEventListener("pagehide", (event) => {
  if (!event.isSameOriginDocumentSwap) return;
  if (looksRight(event.nextPageURL)) {
    // This signals that the outgoing elements should be captured.
    event.pleaseLetTheNextPageDoATransitionPlease();
  }
});
// In the incoming page
document.addEventListener("beforepageshow", (event) => {
  if (
    event.previousPageWantsToDoATransition &&
    looksRight(event.previousPageURL)
  ) {
    const transitionReadyPromise = event.yeahLetsDoAPageTransition();
  }
});

Shared Element Transitions API for cross-document transitions would also need to be heavily restricted for security reasons.

Framework Implementation Examples

During the past few weeks, I saw some jaw-dropping examples of using Shared Element Transitions API for page navigation, added with progressive enhancement to various frameworks like React and Svelte.

Adding page transitions with SET API in frameworks can be tricky. In this example, we’ve had control over the DOM update functions, but this is not usually the case with front-end frameworks. Hopefully, as this API gets proper browser support and traction in the dev community, frameworks and router libraries will follow suit and provide better ways to integrate Shared Element Transitions API in navigation.

So, I would like to highlight some awesome examples of framework implementations from the community, especially those that provide reusable functions and hooks.

React / Preact

Jake Archibald created a great video playlist example using Preact, TypeScript, and a custom page transition hook. This example uses a custom router implementation to apply class names to the html element to customize the animation and toggle different types of animation depending on the navigation direction.

Astro

Maxi Ferreira implemented page transitions similarly as in our example with Navigation API but with Astro and explained the process in great detail on top of building a stunning movie database app.

He also worked with Ben Myers on this awesome guitar shop example with cool animations on both the guitar image and item background, which expands into a full description background container. This is also a good example of how to create elaborate but seamless and tasteful animations that add to the user experience.

Svelte

Moving onto Svelte, Geoff Rich built this neat fruit nutritional data app and explained the whole process in great detail in his article. SvelteKit has a built-in navigating store, and Geoff created a handy util function for intercepting page transitions and applying the Shared Element Transitions API depending on its browser support.

Conclusion

Shared Element Transitions API allows us not only to implement complex UI animations on a component level but also on a page level. Same-document transitions in Single Page Applications can be implemented today with progressive enhancement, and we can achieve impressive app-like page transitions with just a few lines of JavaScript and CSS. And all that without a JavaScript animation library! More popular and more complex cross-document transitions for Multi Page Applications are still a work in progress, and I can see it being a massive game-changer once it’s released and gains wider browser support.

Judging from the impressive examples we’ve seen online, some of which are featured in this article, we can safely say that the community is more than excited about this API. If you’ve built something awesome using Shared Element Transitions API, feel free to reach out on Twitter or LinkedIn and share your work.

Many thanks to Nikola Vranesic for reviewing the article for technical accuracy.

References

  • Shared Element Transitions, WICG
  • “Smooth and simple page transitions with the shared element transition API,” Jake Archibald
  • CSS Shared Element Transitions Module Level 1, W3C