Native Dialogs and the Popover API — What you need to know

The modern web is awesome! We can easily create accessible, robust modal dialogs with the native <dialog> element. Want to open a menu or tooltip on top of the other page content? No problem! The HTML attribute popover turns any element into popover content.

The last few months, I've been building more and more dialogs with the native HTML element. And in April, when the Popover API reached cross-browser support, I started experimenting with popover in various projects. I quickly realized: These web features make our lives as web developers a lot easier, but there are some pitfalls. Especially when you combine both features!

Several pancakes stacked on top of each other. Photo: © Rama Khandkar / pexels.com

I'll quickly go over the basics of how dialogs and popovers work together. Then I'll share some of my hard-earned learnings with you. If you're not familiar with the features in general, I recommend you read my blog posts “Why you should use the Native Dialog Element” and “Make your content pop with the Popover API and CSS Anchor Positioning”.

The Basics: How dialog and popover elements interact

When you open a modal dialog with the showModal() method, the dialog is added to the top layer and rendered on top of other page content. The same happens when you show popover content using the showPopover() method.

Now, what happens if an element is already open in the top layer and you add another element to it? The elements are stacked in the order they are added to the top layer. The last one in always appears on top. You can't use the z-index property to change this stacking order. The only thing that matters is, when an element was added to the top layer.

To give you an example: A button opens a menu panel as a popover. This menu contains an option that opens a modal dialog that is rendered on top of the menu panel. This dialog contains a button that opens a tooltip popover on top of the dialog. We could go on and on. 😉

What you need to know about dialogs and popovers

Animation as progressive enhancement

Everything looks better with smooth entry and exit animations. There's only one problem: Dialogs and popovers are set to display: none when hidden and display: block when shown. And we all know that you can't transition content from or to display: none. Right? Wrong!

The new @starting-style rule together with the transition-behavior CSS property enable us to animate dialogs and popovers. I described both features in detail in my blog post “Accessible Alerts made easy by the Popover API”.

Check out my demo for a modal dialog with smooth fade in and out animation. At the moment of writing, this only works in Chrome and Edge:

You can define transitions for the dialog (or popover) itself as well as its backdrop. Here's the CSS code for animating the dialog:

dialog { --duration: 150ms; --start-opacity: 0.5; --start-scale: scale(0.8); /* End values for fade out. */ opacity: var(--start-opacity); transform: var(--start-scale); transition: opacity var(--duration) ease-out, transform var(--duration) cubic-bezier(0, 0, 0.2, 1), overlay var(--duration) allow-discrete, display var(--duration) allow-discrete; } dialog[open] { /* End values for fade in; start values for fade out. */ opacity: 1; transform: scale(1); @starting-style { /* Start values vor fade in. */ opacity: var(--start-opacity); transform: var(--start-scale); } }

And this is how you animate the dialog's backdrop:

/* Styling for backdrop behind the dialog */ dialog::backdrop { background: rgb(0 0 0 / 0.32); /* End value for fade out. */ opacity: 0; transition: opacity var(--duration), overlay var(--duration) allow-discrete, display var(--duration) allow-discrete; } dialog[open]::backdrop { /* End value for fade in; start value for fade out. */ opacity: 1; } /* This starting-style rule cannot be nested inside the above selector because the nesting selector cannot represent pseudo-elements. */ @starting-style { dialog[open]::backdrop { /* Start value vor fade in. */ opacity: 0; } }

But what about browser support? Don't worry! The @starting-style rule and the transition-behavior property are perfect examples of progressive enhancement. If a browser doesn't support these new features, then it will still render the dialog or popover element, but without the animation.

Automatically close the dialog on backdrop click

The native <dialog> element has several great features built in:

  • When a modal dialog is opened, the browser moves focus to the first interactive element inside of the dialog.
  • Closing the modal dialog returns focus to the element that opened the dialog.
  • Users can close the modal dialog with the ESC key.

But one important feature is not supported by default: Automatically closing the dialog when the user clicks on the backdrop. In my first blog post about dialogs, I demonstrated a custom implementation of this feature: Getting the coordinates of the click and comparing them to the dialog's rectangle.

Some time ago, I came across a more elegant solution: You add a click event listener to your dialog and then check the tag name of the event target. Here's the JavaScript code for the function:

function onDialogClick(event) { event.stopPropagation(); if (event.target.tagName === "DIALOG") { dialogElementRef.close(); } }

A click on the dialog's backdrop is registered as a click on the dialog element. For this to work, the dialog's content needs to be wrapped in an extra element. Otherwise, clicking inside certain areas in the dialog would also close it.

Check out the CodePen demo above where I also implemented this custom behavior.

How to properly nest popover content in modal dialogs

Some time ago, I was working on a web project for a client that includes a list of items. The user can click on one of the items to open a modal dialog with more details. This dialog contains a button that allows the user to add the item to their list of favorites. Afterwards, a snackbar appears on the bottom of the screen that also includes an undo button.

Being the hip, state-of-the-art web developer that I am, I wanted to use the <dialog> element combined with a popover element for the snackbar. After implementing everything, I started to test the undo feature and was baffled: Although the snackbar with the undo button was visible and appeared on top of the modal dialog, I wasn't able to interact with it. Me: “What the hell is happening?!”

After some research, I found the cause of the problem. But before I tell you, check out this demo with a minimal version of the web project I described above. The dialog includes two buttons: The first opens a snackbar that you can't interact with. The second button opens a snackbar with a working undo button. Can you tell the difference?

Let me explain what's going on: In general, the position of a popover element in the DOM doesn't affect its visibility. When you show the popover, the browser adds it to the top layer and it appears on top of all other content. But only because you can see something doesn't mean you can interact with it. The answer can be found in the description of the showModal() method:

The showModal() method of the HTMLDialogElement interface displays the dialog as a modal [...]. Interaction outside the dialog is blocked and the content outside it is rendered inert.

This means, if you place the popover element for the snackbar outside of the dialog, then it's rendered inert. All user input events for the element and its descendants are ignored. So what can you do? Simply place the popover element inside of the dialog element. Now the browser considers it part of the dialog's content and won't render it inert.

Always close manual popovers nested inside dialogs

One last observation about manual popovers nested inside a modal dialog. If you close the dialog, make sure to also close the popover with the hidePopover() method. Otherwise, the popover might not be visible, but would still be open and remain in the top layer.

Now, when you open the dialog again, it will be placed above the popover inside the top layer. So even if you try to open the popover again with the showPopover() method, nothing will happen! The popover is already open and placed beneath the modal dialog in the top layer. What a mess!

Conclusion

The native <dialog> element and the popover attribute are very powerful and convenient features. But there are some pitfalls. I hope my learnings can help you to make great use of these awesome features in your own projects. Happy coding! 😊

Posted on