Skip

Dialog view transitions

In a recent project I was wondering if I can combine two of my favourite modern web features: view transitions and the <dialog> element. Turns out it’s possible and not too complicated.

First, here’s the finished result. Click on a card cat to open the dialog with a smooth transition. You can also try keyboard navigation by tabbing and pressing the enter or space button:

See the Pen View Transition Dialog by Thomas Günther (@medienbaecker) on CodePen.

We’ll first look at the two features, dialog and view transitions, separately, before combining them.

The dialog element

This feature is incredible. In my otherwise ranty article “Trusting the browser” I already mentioned how much I love using the <dialog> element. It has a lot of built-in features that make it accessible and user friendly by default. Apart from all the things that come with it for free, like focus management and the top-layer positioning, there’s also an optional closedby="any" attribute that makes it easy to close dialogs by clicking outside.

Here’s a simple example of such a dialog. Click the button to open it:

See the Pen Dialog by Thomas Günther (@medienbaecker) on CodePen.

To open a dialog, you call its showModal() method. This puts it on the top layer and traps focus inside.

For closing, there’s dialog.close(). If the user clicks outside a closedby="any" dialog or presses Esc, the browser does the closing for you.

MDN’s dialog documentation has more information and examples.

View transitions

View transitions enable animations between states of elements, even across documents. While I’ve also seen a lot of bad examples, it can (and should) be used to enhance user interfaces with purposeful animations. By showing users not just the before and after, but animating between these states.

I’ve always loved these kinds of animations. They are only possible when designers and developers work closely together and understand each other’s work. That’s something I enjoy a lot, as a designer turned developer. Unfortunately, implementing them always came with a lot of overhead. For a long time I’ve had to rely on clever people like David Khourshid explaining the FLIP technique, or use huge JavaScript libraries. I ended up with complicated code that was hard to maintain and often fundamentally inaccessible.

So while these kind of animations are nothing new, native view transitions are removing a lot of flipping overhead. They are supported in all major browsers, but I have been using them in production for quite a while now. A perfect example of progressive enhancement in most cases: Who cares if the animation is missing, the content is still there.

Here’s a simple example of a view transition between two states. Click the cat to make it bigger:

See the Pen View Transition by Thomas Günther (@medienbaecker) on CodePen.

The API itself is straightforward. For same-document view transitions, you wrap a DOM change in document.startViewTransition(), and the browser snapshots before and after. Elements with matching view-transition-name values get morphed between states.

I really enjoyed reading Declan Chidlow’s article with practical examples of view transitions.

Combining them

In this case, the dialog’s showModal() is our DOM change. We just need to wrap it in the view transition API’s startViewTransition() method:

document.startViewTransition(() => {
  dialog.showModal();
});

I clone the card instead of moving it. The original stays in the grid (hidden with visibility: hidden). This keeps the layout intact and gives the closing transition somewhere to animate back to. But this also means the image in the grid and the image in the dialog are different elements.

For the transition to work, we need to hand off the view-transition-name from one to the other:

const prefersReducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;

// 1. Give the source a name
if (!prefersReducedMotion) sourceImg.style.viewTransitionName = "card";

document.startViewTransition(() => {
  // 2. Remove the name from the source
  sourceImg.style.viewTransitionName = "";
  // 3. Add the name to the dialog
  if (!prefersReducedMotion) dialogImg.style.viewTransitionName = "card";
  // 4. Show the dialog
  dialog.showModal();
});

The prefersReducedMotion check skips the view-transition-name assignment for users who prefer reduced motion. The dialog will still animate, but instead of motion, it just fades.

And now we already have a transition when opening the dialog. It’s magical!

But wait, closing it makes it vanish instantly. To animate that, we have to intercept the event and run our own transition first:

dialog.addEventListener("cancel", (e) => {
  // Prevent instant close
  e.preventDefault();

  // 1. Give the dialog a name
  if (!prefersReducedMotion) dialogImg.style.viewTransitionName = "card";

  document.startViewTransition(() => {
    // 2. Remove the name from the dialog
    dialogImg.style.viewTransitionName = "";
    // 3. Add the name to the source
    if (!prefersReducedMotion) sourceImg.style.viewTransitionName = "card";
    // 4. Close the dialog
    dialog.close();
  });
});

This works for any close method, pressing Esc, clicking outside, or good old close buttons.

Now we have fully animated dialog. You can find the full code in the CodePen.

Replies

  • Amadeus Maximilian

    @medienbaecker This is so slick! I remember the effort I had to go through to achieve something like this a couple years back. 😊

    I need to do more with view transitions. 👌

  • Thomas Günther

    @amxmln Right? I even remember digging into PhotoSwipe's source code back in the day to understand how they built the zoom animation. That stuff was complex!