Brad Woods Digital Garden

Notes / JavaScript / Web API / View transition

The Warhammer 40k Adeptus Mechanicus symbol

Table of contents

    View Transition API

    Planted: 

    Status: seed

    Hits: 162

    Intended Audience: Front-end developers

    The View Transition API provides a way to animate elements when moving from one page to another. It is available on all major browsers however Firefox only provides limited support.

    Default transtion

    To use the API, you need to opt in by adding following CSS to each page:

    /index.css

    @view-transition {
    navigation: auto;
    }

    This enables the default cross-fade animation — decreasing and increasing opacity of outgoing and incoming elements.

    Each example's CSS and JavaScript is unminified and embedded within the HTML file:

    Custom transition

    We can change the default animation using CSS selectors:

    /index.css

    @keyframes fade-in {
    from {
    opacity: 0;
    }
    }
    @keyframes fade-out {
    to {
    opacity: 0;
    }
    }
    @keyframes slide-to-left {
    to {
    translate: -120px;
    }
    }
    @keyframes slide-from-right {
    from {
    translate: 120px;
    }
    }
    /* Set the animation for outgoing page elements */
    ::view-transition-old(root) {
    animation:
    slide-to-left 1s,
    fade-out 1s;
    }
    /* Set the animation for incoming page elements */
    ::view-transition-new(root) {
    animation:
    slide-from-right 1s,
    fade-in 1s;
    }

    Page-based transition

    We can also change the animation based on what page we are navigating to. The below example slides elements left when navigating to page A and right when navigating to the index page. It uses:

    • pageswap: an event fired on the outgoing page (before the last frame is rendered).
    • pagereveal: an event fired on the incoming page (after it has been initialized but before the first render).
    • window.navigation: a property to obtain URLs (limited availability). This could be replaced by storing the URL in sessionStorage on the outgoing page and then reading it on the incoming page.

    /index.css

    :root {
    --transition-old: slide-to-left 1s, fade-out 1s;
    --transition-new: slide-from-right 1s, fade-in 1s;
    --transition-old-reverse: slide-to-right 1s, fade-out 1s;
    --transition-new-reverse: slide-from-left 1s, fade-in 1s;
    }
    ::view-transition-old(root) {
    animation: var(--transition-old);
    }
    ::view-transition-new(root) {
    animation: var(--transition-new);
    }

    /index.js

    function isReverseTransition(toURL) {
    /* Using endsWith because I'm iframing the page */
    /* return toURL.pathname === "/index.html"; */
    return toURL.pathname.endsWith("/index.html");
    }
    async function setTemporaryReverseTransition(transitionPromise) {
    const root = document.documentElement;
    root.style.setProperty(
    "--transition-old",
    "var(--transition-old-reverse)",
    );
    root.style.setProperty(
    "--transition-new",
    "var(--transition-new-reverse)",
    );
    await transitionPromise;
    // Clean up
    root.style.removeProperty("--transition-old");
    root.style.removeProperty("--transition-new");
    }
    function onTransition(toURL, evt) {
    if (isReverseTransition(toURL)) {
    setTemporaryReverseTransition(evt.viewTransition.finished);
    }
    }
    window.addEventListener("pageswap", async (evt) => {
    // Not used: just demonstrating how to access
    const fromURL = new URL(evt.activation.from.url);
    const toURL = new URL(evt.activation.entry.url);
    onTransition(toURL, evt);
    });
    window.addEventListener("pagereveal", async (evt) => {
    const toURL = new URL(window.navigation.activation.entry.url);
    /* evt.viewTransition doesn't exist on page load (pagereveal will trigger on page load) */
    if (evt.viewTransition) {
    onTransition(toURL, evt);
    }
    });

    Connect elements

    We can connect elements on different pages by giving both the same view-transition-name. This results in an animation where the outgoing element morphs into the incoming element.

    /index.css

    .greenSquare {
    width: 100px;
    aspect-ratio: 1;
    margin-right: 240px;
    background-color: green;
    view-transition-name: my-transition;
    }
    .redSquare {
    width: 200px;
    aspect-ratio: 1;
    margin-left: 240px;
    background-color: red;
    view-transition-name: my-transition;
    }

    Multiple transitions

    We can use more than one animation during a transition. Below I've:

    • customized the default animation duration to two seconds — used by the anchors.
    • created a new transition: my-transition — used by the squares.

    /index.css

    .greenSquare,
    .redSquare {
    ...
    view-transition-name: my-transition;
    }
    ::view-transition-old(root) {
    animation-duration: 2s;
    }
    ::view-transition-new(root) {
    animation-duration: 2s;
    }
    ::view-transition-old(my-transition) {
    animation:
    slide-to-left 1s,
    fade-out 1s;
    }
    ::view-transition-new(my-transition) {
    animation:
    slide-from-right 1s,
    fade-in 1s;
    }

    UI example: menu

    An ideal use case for the View Transition API is animating from a menu to a selected item page. The menu has a small image that enlarges on the item page. This can be achieved by:

    • on each item page, set a view-transition-name to the image element.
    • temporarily set the view-transition-name to a menu item's image:
      • when a menu item is clicked.
      • when navigating back to the menu page using the pagereveal event.

    /index.js

    async function setTemporaryReverseTransition(
    elem,
    transitionPromise,
    ) {
    elem.style.viewTransitionName = "my-transition";
    // Cleanup
    await transitionPromise;
    elem.style.removeProperty("view-transition-name");
    }
    window.addEventListener("pagereveal", async (evt) => {
    const fromURL = new URL(navigation.activation.from.url);
    const { pathname } = fromURL;
    const parts = pathname.split("/");
    const anchor = document.querySelector(
    `a[href="${parts.pop()}"]`,
    );
    // Required because pagereveal event triggers on initial page load
    if (!anchor) return;
    const sibling = anchor.nextElementSibling;
    setTemporaryReverseTransition(
    sibling,
    evt.viewTransition.finished,
    );
    });
    // Add transition name to square when anchor clicked
    document.addEventListener("DOMContentLoaded", () => {
    const anchors = document.querySelectorAll("a");
    anchors.forEach((anchor) => {
    anchor.addEventListener("click", (evt) => {
    // Capture the square
    const sibling = anchor.nextElementSibling;
    sibling.style.viewTransitionName = "my-transition";
    });
    });
    });

    Overflow: hidden

    An issue with the View Transition API is animating elements contained in a parent with overflow: hidden will break out during a transition. Below the squares are contained within a circular div with overflow: hidden. When the transition occurs, the squares break out of the circle.

    Feedback

    Have any feedback about this note or just want to comment on the state of the economy?

    Where to next?

    JavaScript
    Web API
    Arrow pointing downYOU ARE HERE
    A Johnny Cab pilot