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.
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
<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.
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.
-
We use the
request-closecommand instead of theclosecommand as theclosecommand doesn’t trigger the"cancel"event. -
We animate the button and the button label separately. The button is transitioned towards the dialog panel shape, and the button label is transitioned towards the dialog title shape. If we’d transition the button as a whole the text in the button is scaled as well, which doesn’t look great.
-
The CSS below prevents the user from scrolling the page while the dialog is open.
dialog {
overscroll-behavior: contain;
}
dialog::backdrop {
overflow: hidden;
overscroll-behavior: contain;
}
- You can tune the transition animations to your liking using the various
::view-transitionpseudo elements.
That’s it!