Wasn’t it great — nay, amazing – the day we got a native HTML <dialog> element? It’s, what now, nearly ten years old? Even so, I always find myself looking up how it works, when to use it, ideas for styling it, and whatnot. There’s a lot of nuance to this seemingly little piece of web architecture and it’s high time I give it a proper look… for future Geoff, but maybe future you as well.

Marking up a <dialog>

This is the basic markup:

<button id="dialog-button">Open Dialog</button> <dialog id="dialog">...</dialog>
It won’t open by default. We could manually set the open attribute:

<dialog id="dialog-button" open>...</dialog>But when do you ever want to open a dialing by default? I’m sure it’s a rare use case. Instead, we have a JavaScript open() method for that. We can set up variables for the dialog and button, then invoke the method:

const dialogButton = document.querySelector('#dialog-button'); const dialog = document.querySelect('#dialog'); formButton.addEventListener('click', () => { dialog.show(); })
That works, but buyer beware — that treats the dialog as more of a pop-up than a modal, and modal is what I think you’ll want in most cases. The key differences? A modal includes a backdrop, is automatically positioned in the center of the page, and allows the Esc key to close it.

Notice how show() lacks the backdrop, positioning, and closing stuff:

So, maybe invoke the showModal() method instead:

const dialogButton = document.querySelector('#dialog-button'); const formDialog = document.querySelector('#dialog'); formButton.addEventListener('click', () => { formDialog.showModal(); })
Great! It opens, positions… and closes:

More about closing

Now we can hit the Esc key when the dialog is in focus (and notice how it’s in focus by default) to close the dialog. But if we want some UI that closes the dialog, like a button, we can add that inside the element:

```
Open Dialog


Close


`` That doesn’t work right out of the box but I bet you’ve already guessed how. There’s aclose()` method for it:

const formButton = document.querySelector('#dialog-button'); const formDialog = document.querySelector('#dialog'); const formClose = document.querySelector('#dialog-close'); formButton.addEventListener('click', () => { formDialog.showModal(); }) formClose.addEventListener('click', () => { formDialog.close(); })
I think it’s a little funny/weird that there isn’t a corresponding closeModal(). No worries, though, because close() just works:

And if you want a JavasScript-less approach, we can actually do it declaratively directly in the HTML:

```



Close dialog


```
I can’t vouch for how that affects semantics, but it is indeed possible to close that sucker without JavaScript.

Invoker Commands

As long as we’re talking about declarative closing, we may as well mention an evolving feature called invoker commands that are designed specifically to open and close dialogs declaratively. It’s all totally experimental as I write this, but check this out:

<button command="show-modal" commandfor="my-dialog">Show Dialog</button> <dialog id="my-dialog">...</dialog>
That’s right! We will be able to hook up the button to the dialog directly in HTML with the command and and commandfor attributes to invoke a specific dialog (#my-dialog) to show-modal.

Cool, and that’s true for a closing button as well:

```


Close Dialog


```
And, as Danny explains here, we can hook all that up with JavasScript if we need to listen to those commands and fire off some event when they happen:

// Select all dialogs const dialogs = document.querySelectorAll("dialog"); // Loop all dialogs dialogs.forEach(dialog => { // Listen for close (as normal) dialog.addEventListener("close", () => { // Dialog was closed }); // Listen for command dialog.addEventListener("command", event => { // If command is show-modal if (event.command == "show-modal") { // Dialog was shown (modally) } // Another way to listen for close else if (event.command == "close") { // Dialog was closed } }); });
Keep on eye on that support!

About button labeling

You might be tempted to use an “X” (or at least an SVG icon for it) for the close button’s label:

<button id="dialog-button">Open Dialog</button> <dialog id="dialog"> <button id="dialog-close">X</button> </dialog>
…but that’s not exactly the best thing for screenreaders to announce. We still want it to say “Close Dialog” or something to that effect. So, if you’re keen on using an “X” icon, I’d add a <span> containing the text we want read and visually hide it while preventing the icon from being announced using the aria-hidden attribute:

<button id="form-button">Open Dialog</button> <dialog id="form-dialog"> <button id="form-close"> <span class="visually-hidden">Close modal</span> <span aria-hidden="true"></span> </button> </dialog>
One more accessibility-minded note. See how the close button gets focus when the dialog opens up?

You may or may not want that because now the button could unexpectedly close the dialog if the Space key is accidentally hit. Not the end of the world because closing a modal isn’t exactly a destructive thing and can be opened back up. But if you have other focusable elements in the dialog — perhaps a link or a form field — then maybe consider giving one of those initial focus with the tabindex attribute.

Innate inertness

I want to get into styling the backdrop, but before that, I think it’s worth noting that the page behind an open dialog is inert. In other words, any sort of interaction — text selection, button clicking, focus, inputs, etc. — are unavailable. That’s probably what you want anyway, so it’s good that isn’t something that has to be configured by default. You just won’t actually see the inert attribute in the markup when it happens.

But that’s only when the dialog is setup as a model. Remember the very first demo? We used the show() method to open the dialog on click rather than the more explicit showModal(). That means the first demo is showing off something more like a popover (think tooltips) than a true attention-hoarding modal that traps focus and sits on the very top layer.

You might wonder about competing dialogs, like say an a popover and modal that are open at the same time. Well, how did you open them at the same time in the first place? You’d have to open the dialog popover first since that does not trigger inert behavior. Only the modal dialog does. And when the modal dialog is open, the popover dialog is not in the top layer, making it inaccessible.

Anyway, let’s get to styling!

Styling the backdrop

Let’s start here because I think it’s incredibly hard to even see the backdrop the way it’s styled by default. If you open dialog in the last example, notice that the page background is slightly tinted. Not much, though. That’s the backdrop.

Very subtle. We can style that ourselves with the ::backdrop pseudo-element. For example, we could go full-on solid color:

Buuuuut now we’re obscuring the entire page behind it. That might be OK, but I also think a little transparency, perhaps with a little blur() action can’t hurt because, you know, context:

I actually really like Mojtaba’s background image example in the CSS-Tricks Almanac, even if it contradicts my feelings about obscuring the rest of the page:

Styling the border and background

Two very obvious defaults are setting the <dialog>‘s styling: a vanilla white background and big ol’ black border. Totally fine to leave that as-is if you’d like. Or, not.

You might think your custom styles would go right on the <dialog> element:

/* 👎 */ dialog { background-color: gold; border: 0; border-radius: 12px; }
But you actually want to select it in its open state:

details { /* ... */ &:open { background-color: gold; border: 0; border-radius: 12px; } }
You may have noticed in the DevTools screenshot up there that the :modal pseudo-class has even higher specificity than :open. You can totally use that as well should you need overrides to the overrides.

Watch out for that :open pseudo-class, though. Safari 26.5 just gained support for it the day I’m writing this. If you need deeper support, consider selecting the [open] attribute instead… or just using :modal.

Styling the position

A less obvious default dialog style is how it’s positioned in the center of the viewport. Open DevTools and you’ll see the UA styling that does that:

We could override margin-top to make the dialog a little more snug with the top of the viewport:

One thing you probably don’t want to do is override your dialog’s display. It’s set to display: none in its initial closed state. Set that to something like block on the element itself and you totally lose the whole point of having a modal — the whole closed by default thing. You still get basic opening and closing, only without the handy Esc key affordance.

And notice how the custom styles are only applied on the :open state since that’s where they live:

Prevent scrolling when open

Chances are that you don’t want the content behind the ::backdrop to scroll. It’s one of those situations where a user can be taken out of context and placed somewhere totally different on the page than where they were when opening the dialog.

It’d be really nice if the underlying content was stuck in place by default, but it’s perfectly understandable why it doesn’t: a dialog is not a scroll container. If it was, we could slap overscroll-behavior: contain on it and be done with it.

Well, turns out that Chrome 144 tweaked that up a bit so that overscroll-behavior works on non-scrollable scroll containers. So, assuming we’re in Chrome 144+, we can set that behavior on the dialog and its backdrop:

dialog { overscroll-behavior: contain; &::backdrop { overscroll-behavior: contain; } }
The last missing piece is that we need to make the dialog itself a scroll container:

dialog { overflow: hidden; overscroll-behavior: contain; &::backdrop { overscroll-behavior: contain; } }
Chrome 144 or above needed:

That’s cool and all, but another (and more concise) way to do it with broad browser support is to check if the body element :has() a dialog with an open attribute. And if it does, we hide the body overflow:

body:has(dialog[open]) { overflow: hidden }
That said, I do like the overscroll-behavior approach because it’s more declarative and attached to the element we’re selecting.

Getting creative with dialog styling

Andy Clarke has a complete write-up on creative ways to style dialogs beyond the basic content-in-box. It’s well out of scope of what we’re covering here, but well worth the read. Here’s one example to whet your appetite:

Animating dialogs

Dialogs kinda “snap” in and out when they’re opened and closed. But we can sprinkle in a little animation for when they enter and exit view.

Like, what if we made the dialog slowly fade in instead. You might think this would work:

/* Nope! 👎 */ dialog { opacity: 0; overflow: hidden; overscroll-behavior: contain; transition: opacity .5s ease-in-out; width: 80vw; &:open { opacity: 1; } }
But, no. We have to explicitly set a starting style for elements just as they are rendered in the DOM. In this case, a dialog is display: none by default and has no opacity set on it when it is activated. That’s where the @starting-style at-rule comes into play:

/* Yep! 👍 */ @starting-style { dialog:open { opacity: 0; } } dialog { overflow: hidden; overscroll-behavior: contain; transition: opacity .5s ease-in-out; width: 80vw; &:open { opacity: 1; } }
There we go:

Entering and exiting view? That actually sounds like prime View Transitions API territory! But, alas, dialogs are not a great use case for them. Why? Modal dialogs live in the top layer, and closing them can remove them in a way that does not always produce a reliable old/new pair for the named transition.

Here’s an example of that where we have a view-transition-name set on the dialog element and then the ::view-transition-new() and ::view-transition-old() states bound to that name, each calling an animation that slides in and slides out, respectively. Works well for the starting transition, but not so much for the exiting transition. Notice, too, that the backdrop needs extra work since it’s included in the mix:

What you can do instead is some sort of hybrid approach by setting the view transition on the open state and using a CSS animation on the close state.

Or maybe just use CSS animations/transitions for both states! I’m not sure there’s any real added value in using a view transition on one state for the sake of using a view transition.

Anyway, if you’re looking to get more creative with in-n-out animations, Chris Coyier has a pretty cool one where the modal follows a shape() path. His demonstrates a dialog configured as a popover, so I forked it and used a modal instead:

Dialog or Popover?

Which one should you use? It’s a really good question because the Dialog API and Popover API are super similar but designed for different use cases. Zell Liew has a concise answer:

After

~~a bit~~lots of research, I discovered that the Popover API and Dialog API are wildly different in terms of accessibility. So, if you’re trying to decide whether to use Popover API or Dialog’s API, I recommend you:

  • Use Popover API for most popovers.
  • Use Dialog’s API only for modal dialogs.

The “in terms of accessibility” is what really matters here because, popovers lack:

  • automatic focus management,
  • automatic ARIA connection, and
  • automatic light dismiss

Meanwhile, a dialog:

  • automatically inerts other elements,
  • prevents users from tabbing into other elements, and
  • prevents screen readers from reaching other elements.

So, if you’re planning to use a popover and need accessible affordances for trapping focus and making other elements inert, you’ll need to handle those on your own in JavaScript.

Zell also notes that popovers need an explicit accessible role. And there are several to choose from so it’s gonna take some thought to choose the right one.

This isn’t all to say, hey, always use a dialog. It’s more about choosing the right API for the right use case. Quoting Zell again:

Popoveris an umbrella term for any kind of on-demand popup.
Dialogis one type of popover — a kind that creates a new window (or card) to contain some content.

Wrapping up

That’s all for now. I’ll update this if y’all have more to add or better or more accurate ways to articulate what’s here. Future us-es (there’s no plural for us, right?) will thank us later.