Discover Dialog

by Sara Joy published on

Suffering modal woes? Positioning, backdrops, focus trapping, z-index — Oof.

Just as I was, some of you may have been coding a lot of these functions into a <div> by hand with CSS and JavaScript, or using a library to handle them for you.

Either way, things are getting simpler and more declarative all the time - and when we're so busy working, we don't always realise how much has changed! Maybe this will help you shave off some lines of code or remove some dependencies from your codebase.

To those of you new to building on the web, this is for you too, as you lucky things get to skip the old ways :)

Meet

Hello 👋
Nice to meet you!

The dialog element has been available across browsers since March 2022, and allows you to pop a little overlay on your page - a common variant is known as a modal - and it has a whole bunch of abilities built in, with further useful facets still being added.

<dialog id="meetDialog">
<p>
Hello 👋
<br />Nice to meet you!
</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>

We're probably more familiar with the modal variety of dialog, intended to (hopefully only briefly) stop you interacting with the page under it. But there are also plenty of non-modal dialogs to be seen around the web, for example notifications.

Pop! Do I look
a little burned?

The dialog element gives us both modal and non-modal variants, depending on how we open them. In the case of more than one dialog being opened (assuming no close action happening on the previous dialogs), the most recently opened dialog will appear on top.

Opening with JS

To open a dialog, we use one of two JavaScript functions on a dialog element:

.show()
.showModal()

Likely based on the click of a button, you will run something like the following (you will likely use a much more specific querySelector):

document.querySelector("dialog").show();
// or
document.querySelector("dialog").showModal();

💡 There is a declarative method available to invoke dialogs, but for now I will stick to what is currently available across the main browsers.

In the CodePen below, both kinds of dialog opening method are available. You'll notice that you can still interact with the Open Modal button after having opened the dialog, but not vice-versa. The modal can be closed with the Esc key, the non-modal dialog cannot.

See the Pen Dialogs! by Sara (@sarajw) on CodePen.

Closing dialogs

You'll also not see any JS for the closure of the dialogs. Pop into the HTML tab in the demo, and you might spot the method="dialog" in the <form> element wrapping the <button>. Here I'm only using it to close the dialogs with said button. If you include more inputs here, method="dialog" will send a submit event and close the dialog, but not actually submit any data anywhere.

<form method="dialog">
<button>Close</button>
</form>

Dialogs can also be closed without using this form method, instead by activating some JS (maybe also with a button):

document.querySelector("dialog").close();
// or
document.querySelector("dialog").requestClose();

This requestClose() is a newly available addition, allowing access to the cancel event, otherwise thrown before the close event when exiting a modal with the Esc key. This may be useful to create "are you sure?" type interactions. (Please do so with care if you must! We'll have no deceptive patterns here..)

Light dismissal

You're probably already trying to close the dialogs by clicking outside of them, which is a common pattern and is often referred to as light dismiss.

This isn't available by default, but can be coded in with CSS and JS.

With thanks to Chris Ferdinandi and Konnor Rogers for the inspiration, I find the following code works well for modal dialogs only, through the magic of the ::backdrop:

/* disallow pointer events on the backdrop, so they
don't register as clicks on the dialog itself */

dialog::backdrop {
pointer-events: none;
}
document.addEventListener("click", (event) => {
// find any open modal dialogs
const openDialogs = document.querySelectorAll("dialog[open]:modal");
// return if none open
if (!openDialogs) return;

// check for clicks on documentElement, passed through the backdrop
if (event.target === document.documentElement) {
// close the open dialogs
openDialogs.forEach((dialog) => dialog.close());
}
});

See the Pen Dialogs with light dismiss (JS) by Sara (@sarajw) on CodePen.

💡 You may have spotted we selected dialog[open] in the JS above.

Yes, open dialogs do get open added as an attribute, and it is possible to toggle this to open and close a dialog, but it is not recommended. It will always open as a non-modal dialog when done in this way, and may lead to confused or missing close events.

I am selecting :modal dialogs only in the code and demonstration above - but you can omit this if you want the click-away to also close any non-modal dialogs that happen to be open at the same time.

To achieve light dismiss on non-modal dialogs alone is trickier unfortunately, as they have no backdrop to play with...

But there is some light at the end of the dismiss tunnel with the closedby property, which is very nearly available across the big browsers - so please feel free to poke any friendly Safari developers you might know! Then it could look as simple as the below:

See the Pen Dialogs with light dismiss (closedby) by Sara (@sarajw) on CodePen.

(Some) styling built in

Apart from limiting the piratey lorem ipsum column width and centering it, no other custom styling has occurred in the CodePen demos.

By default, the non-modal dialog has no backdrop, while the modal dialog's ::backdrop has a subtle transparent grey, and the dialog itself is centered vertically as well as horizontally on the page.

I am going to stand on the shoulders of giants here, and suggest some places you can go to look at amazing dialog CSS styling and animation:

Scrollin', scrollin', scrollin'...

That by default the website under your modal is still scrollable may or may not bother you. It bothers me!

There are ways and means to stop that happening, thankfully.

A classic and elegant way made possible with the sort-of-new-but-well-supported :has() pseudo-class and scrollbar-gutter property is the following:

/* Check whether any modal dialogs are open */
html:has(dialog[open]:modal) {
/* Poof! No more scrolling! */
overflow: hidden;
/* keep the scrollbar width */
scrollbar-gutter: stable;
}

The scrollbar-gutter property keeps the text on the page from reflowing with a jerk, when it suddenly becomes one scrollbar-width wider as the rest of the page beyond the viewport is hidden.

💡 If you have both non-modal and modal dialogs on your page, be sure to use :modal in your selector so this only triggers on open modal dialogs.

See the Pen Modal dialog with scroll-stop by Sara (@sarajw) on CodePen.

You may have noticed - if you can see a scrollbar - that the scrollbar gutter isn't taking on the backdrop style. Yeah. Go ahead and comment out the scrollbar-gutter: stable; line in the CSS, open/close the modal a few times, and see whether the scrollbar appearing and disappearing bothers you more. It might not - but it bothers me!

Reinstate scrollbar-gutter: stable;, then scroll down a little in the CSS of the above CodePen and uncomment the following:

dialog[open]::backdrop {
background-color: transparent;
backdrop-filter: blur(0.25rem);
}

Here I've dropped the default shading of the ::backdrop and applied a blur instead - meaning the gutter blends in, assuming the gutter and page are both the same colour. You might even have some fun seeing how the scrollbars change with color-scheme...

Honestly though, different browsers and OSes and dark and light modes and device types make scroll bars look different all over the place.

On a MacBook whether you even see the scrollbar when you're not actively scrolling depends on whether you have an external pointing device connected or not! So many people now browse the web with mobile devices, where the scrollbars and gutters are largely hidden - so it likely isn't worth spending too much effort trying to "fix" this. Trust me. Don't waste your time on it, I've wasted enough for both of us!

Accessibility

As it opens, by default the focus will land on either the whole dialog, or if it has one, the first focusable element (whether it's a close button or not). A sighted user with a pointing device will probably not tend to notice where the focus lands - but it's very important as a screen reader user, as the focus determines where in a document the reader lands and begins reading from.

In case of needing the focus to land somewhere specific for your use case, I heartily recommend reading Where to Put Focus When Opening a Modal Dialog by Adrian Roselli to help you decide where the focus will land on your dialog. Then read O dialog focus, where art thou? to help you consistently get it where you want it.

If you read the MDN page on dialogs and are bothered by this warning about tabindex on <dialog>, you may either take Adrian or Manuel's word that it's OK, or you can enjoy Manuel's deep dive into What's an interactive element?. Enjoy!

Dialogs discovered

I hope this has been enough to convince you that it's worth using dialogs, if you aren't already, or maybe you've found a couple of interesting tidbits you otherwise weren't aware of.

HTML is still changing and improving - it's not at all archaic or just there to provide <div>s into which we can inject all the things with JavaScript (though of course you can still do that, just please inject accessible elements)!

Feel free to contact me on the social medias to start a, uh, dialog.

About Sara Joy

Sara has been extremely online since 1998, making her own personal websites since 1999. She switched her career from electronic engineering to front end web development in 2022. She loves the web platform, and wants it to be accessible to everyone.

Homepage: sarajoy.dev