Animating The Dialog Element Using View Transitions

In this article we look at animating the <dialog> element using the View Transition API. We’ll follow progressive enhancement so it’s only used when supported and when the user allows animations to run.

Let’s take a look at the non-view-transition demo below. This demo uses the Invoker Commands API and doesn’t require any JavaScript. Click the button to open the dialog.

Hello World

As we can see below, the HTML is very straightforward and reveals the Invoker Commands API. The command attribute describes the action we want to run, and the commandfor attribute links a button to a target element using the id attribute of the target.

The "show-modal" command opens the dialog as a modal, and "request-close" close the modal. Note that we can also press the ESC key to close the modal.

<button command="show-modal" commandfor="my-dialog">Hello World</button>

<dialog id="my-dialog">
    <span>Hello World</span>
    <button command="request-close" commandfor="my-dialog">Close</button>
</dialog>

With the basic functionality set up, let’s look at adding the View Transitions API.

Adding The View Transitions API

Using the view transitions API we can create an animation that makes it seem like the dialog originated from the button, this creates a visual relationship between the two elements, helping communicate intent.

I’m currently implementing this in FilePond v5 to better communicate the relationship between an input source button and the modal that it triggers. An example of this would be a camera input button that allows you to use your webcam or phone camera to upload a photo.

The button below triggers a dialog using View Transitions. Please note that it doesn’t do so if your browser doesn’t support view transitions or if you’ve configured your system to reduce motion.

Hello World

Let’s look at the JavaScript code.

// these are basic helper functions to set the viewTransitionName style property
function clearTransitionName(...targets) {
    targets.forEach(target => target.style.viewTransitionName = '');
}

function setTransitionName(target, name) {
    target.style.viewTransitionName = name;
}

// don't run code if user prefers no animations, or if view transitions not supported
const userAcceptsMotion = matchMedia('prefers-reduced-motion').matches;
const browserSupportsViewTransitions = document.startViewTransition;
if (userAcceptsMotion && browserSupportsViewTransitions) {
    
    // here we get the element references for easy of use later
    const button = document.getElementById('my-button');
    const buttonLabel = button.querySelector('span');
    
    const dialog = document.getElementById('my-dialog');
    const dialogTitle = dialog.querySelector('span');
    
    // we set the initial view transition names, we'll move these to the dialog when the transition starts
    setTransitionName(button, 'panel');
    setTransitionName(buttonLabel, 'title');
    
    // here we intercept events that open the modal
    dialog.addEventListener('command', (e) => {
    
        // only deal with the "show-modal" command
        if (e.command !== 'show-modal') {
            return;
        }

        // don't execute the default browse "show-modal" logic, we'll run it ourselves when we've started the view transition
        e.preventDefault();
        
        // here we start the "open dialog" view transition
        document.startViewTransition(() => {

            // move the transition names from button to dialog
            clearTransitionName(button, buttonLabel);
            setTransitionName(dialog, 'panel');
            setTransitionName(dialogTitle, 'title');

            // now we run the "show-modal" logic
            dialog.showModal();
        });
    });
    
    // here we intercept events that close the modal
    dialog.addEventListener('cancel', (e) => {
        
        // don't execute the default browser action
        e.preventDefault();
    
        // here we start the "close dialog" view transition
        document.startViewTransition(() => {

            // move the transition names from dialog to button
            clearTransitionName(dialog, dialogTitle);
            setTransitionName(button, 'panel');
            setTransitionName(buttonLabel, 'title');

            // now we run the "close" logic
            dialog.close();
        });
    });
}

Some loose ends to discuss.

dialog {
    overscroll-behavior: contain;
}

dialog::backdrop {
    overflow: hidden;
    overscroll-behavior: contain;
}

That’s it!

I share web dev tips on Bluesky, if you found this interesting and want to learn more, follow me thereBluesky

Or join my newsletter

More articles More articles