The View Transition API: A New Way to Animate Page Transitions

Guillaume Pierson
Guillaume PiersonDecember 21, 2023
#css#react

The web is going forward and pages are becoming more and more dynamic. We can see a lot of animations and transitions on websites. It's a good thing because it helps to have a better visual experience. But it's not always easy to implement and maintain. That's why the View Transition API is here to help us. It's an experimental API for view transition that is active on Chromium-based browsers. It allows us to have better control of the transition between two pages.

What Is a View Transition?

A view transition is a visual effect that smoothly changes from one page to another. The browser identifies differences between pages and creates animations, like fading or more complex effects.

View transitions are common in mobile applications. The View Transition API aims to bring this experience to the web.

The following demo shows a security test with 3 questions. Each question is a page. When users answer a question, the page will transition to the next question. Note that the transitions aren't designed to be beautiful, but to show the differences between pages!

View Transitions Basics

To use view transitions, you need to put the page rendering logic in a function and call this function with document.startViewTransition.

const updateDOM = async () => {
    // Render the new page here
};

if (!document.startViewTransition) {
    // Fallback for browsers that don't support View Transitions:
    updateDOM();
} else {
    // Apply the DOM change with View Transitions:
    document.startViewTransition(updateDOM);
}

The browser will compare the DOM before and after the transition, and animate the differences.

Elements present on both the old and new DOM will move from their original to their new positions. Elements only on the new DOM will appear with a fade-in effect. Elements only on the old DOM will disappear with a fade-out effect.

The following demo shows a simple transition between full-page images using a fade-in/fade-out transition (source code).

Customizing Transitions

The View Transition API allows us to customize the transition by using CSS. Transitions are just CSS animations.

Browsers supporting View transitions introduce two new CSS selectors: ::view-transition-old and ::view-transition-new. As their name suggests, these pseudo-elements represent the elements that are only on the old DOM or only on the new DOM.

For instance, to animate the whole page, change the animations on the root transition name:

::view-transition-old(root) {
    animation-duration: 1s;
}
::view-transition-new(root) {
    animation-duration: 3s;
}

Both animations will be played at the same time.

Check the Demo and the source code.

Animating An Element Present On Both Pages

In the previous example, you may have noticed the root name in ::view-transition-old(root). This represents the animation to play for the whole page.

We can create different animations for different elements by giving them a unique transition name. In the following example, we will make a custom animation for an image description.

/* Define the animations with keyframes */
/* Animations taken from https://animista.net */
@keyframes roll-out-left {
    0% {
        transform: translateX(0) rotate(0deg);
        opacity: 1;
    }
    100% {
        transform: translateX(-1000px) rotate(-540deg);
        opacity: 0;
    }
}
@keyframes tilt-in-fwd-tr {
    0% {
        transform: rotateY(20deg) rotateX(35deg) translate(300px, -300px) skew(
                -35deg,
                10deg
            );
        opacity: 0;
    }
    100% {
        transform: rotateY(0) rotateX(0deg) translate(0, 0) skew(0deg, 0deg);
        opacity: 1;
    }
}

/* Animate the header using the headerT transition name */
header {
    view-transition-name: headerT;
}

/* Define the animation for the transition */
::view-transition-old(headerT) {
    animation: roll-out-left 0.6s ease-in both;
}
::view-transition-new(headerT) {
    animation: tilt-in-fwd-tr 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}

The example has some more CSS to customize the header, but here is the result of the same animation.

Demo and the source code.

You can't have multiple elements with the same transition name. If you do so, animations will not be played at all. But DOM changes will be updated. And while animations are still playing the page is not interactive.

Moving Elements Between Pages

In this example, we will move one element from the old page to the new page. The item in the list will become the main object of the page.

This works by using the same transition name on both pages. The browser will try to match the elements with the same transition name and animate them.

<body>
    <div id="container">
        <div class="item" onClick="selectImage('nature')">
            <img
                src="./nature.jpeg"
                class="short nature"
                alt="A beautiful image of nature"
            />
            <h1>A beautiful image of nature</h1>
        </div>
        <div class="item" onClick="selectImage('water')">
            <img
                src="./water.jpeg"
                class="short water"
                alt="A beautiful image of water"
            />
            <h1>A beautiful image of water</h1>
        </div>
    </div>
</body>
.water {
    view-transition-name: water-transition;
}
.nature {
    view-transition-name: nature-transition;
}
const replaceDomWithImage = async topic => {
    const container = document.querySelector('#container');
    container.innerHTML = `
  <div id="header-wrapper">
      <header>
          <h1>A beautiful image of nature</h1>
      </header>
  </div>
  <img
    src="./${topic}.jpeg"
    class="full ${topic}"
    alt="A beautiful image of ${topic}"
    onClick="unSelectImage()"
  />
`;
};

const selectImage = async topic => {
    if (document.startViewTransition) {
        document.startViewTransition(async () => {
            await replaceDomWithImage(topic);
        });
    } else {
        await replaceDomWithImage(topic);
    }
};

const replaceDomWithItems = async topic => {
    const container = document.querySelector('#container');
    container.innerHTML = `
<div class="item" onClick="selectImage('nature')">
  <img
    src="./nature.jpeg"
    class="short nature"
    alt="A beautiful image of nature"
  />
  <h1>A beautiful image of nature</h1>
</div>
<div class="item" onClick="selectImage('water')">
  <img
    src="./water.jpeg"
    class="short water"
    alt="A beautiful image of water"
  />
  <h1>A beautiful image of water</h1>
</div>
`;
};

const unSelectImage = async () => {
    if (document.startViewTransition) {
        document.startViewTransition(async () => {
            await replaceDomWithItems();
        });
    } else {
        await replaceDomWithItems();
    }
};

See the Demo and source code.

And that's it. With the presented elements, you can already do beautiful view transitions. There are many more details to discover. For example, startViewTransition does return an object. The object contains the state of the animation and promises of completion.

For more advanced transitions, I invite you to check the chrome developer page on view transitions and the MDN page on view transitions.

View Transitions In React with React Router or Remix

View Transitions are supported by React Router since v6.17.0 and Remix since v2.1.0, behind an experimental flag.

As React Router and Remix are using the same API, we will use React Router for the following example, but it's the same for Remix.

React Router added a prop called unstable_viewTransition in the <Link> and <NavLink> components. When using <NavLink>, this property appends the class transitioning to the link when the transition is starting. It allows to customize the link during the transition, and to have custom transitions depending on the link.

The following example shows how to use unstable_viewTransition with <NavLink> and CSS.

function TransitionLink(to) {
    return <NavLink to={to} unstable_viewTransition />;
}
a.transitioning {
    transition-name: image-expand;
}

You can also use a new hook called unstable_useViewTransitionState(addressTo) that returns true if the navigation is in progress to the given address.

Here is an example of an image link that expands when the user clicks on it.

function ImageLink(to, src, alt) {
    const isTransitioning = unstable_useViewTransitionState(to);
    return (
        <Link to={to} unstable_viewTransition>
            <img
                src={src}
                alt={alt}
                style={{
                    viewTransitionName: isTransitioning ? 'image-expand' : '',
                }}
            />
        </Link>
    );
}

But you don't necessarily need to use the hook, you can just use the <Link> component as usual and use basic CSS to customize the transition. It's useful to deduplicate the code if you want to animate an item in a list.

I made some riddles with React Router and the View Transition API. You can check the source code on Github and the demo on Github Pages.

Conclusion

The View Transition API is a great way to make beautiful transitions between pages. It's easy to use and it's already supported by Chromium-based browsers. It's a good way to make your website more dynamic and beautiful.

But it's still an experimental API, so it's not recommended to use it in production. Also, one of the major drawbacks is that the page is not interactive while the animation is playing. So it's not recommended to use it for a long animation, but it's perfect for a short animation.

You can already use it in your side projects or for fun.

Did you like this article? Share it!