I was on the epicgames.com website the other day, signing up so I could relive my Magic: The Gathering glory days with Arena. While doing that I saw their style for modal dialogs and thought I should try to re-create that with

because apparently I’m both of those types of nerd.

Screenshot 2025 04 14 at 8.20.15%E2%80%AFAM

It’s a

This thing came up on top of other content, so that alone makes it appropriate for the HTML

element. We’ll use that.

No Taller than Viewport

It’s not absolutely required that the entire

needs to be shorter than the viewport. If it’s taller, you just need to be able to scroll to all of the content it contains. The default styling for allows for that.

But I would argue that if you’re putting actions that relate to the content of the dialog at the bottom then you should limit the height of the dialog to the viewport so that those actions are always visible. If a dialog simply has an ✕ close button on the top, maybe it doesn’t matter, but here we’ve got important buttons at the bottom, so it does.

The default styling for dialog includes position: absolute; and we can keep that while limiting the height like:

dialog {
  block-size: 90dvb;
  inset-block-start: 5dvb;
}Code language: CSS (css)

That will limit the height to essentially 90% of the viewport height (the dvb part means “dynamic viewport size in the block direction”). I like the “dynamic” sizing units because it means that it accommodates browser “chrome” (toolbars and stuff) being present (or not). The inset amount is half of what’s left over, so essentially vertical centering.

100dvh adapts itself be 91c728b09836d 1920
This graphic convinces me dynamic viewport height units are a good idea. (source)

Note that the dialog element’s default styles can be a bit confusing and you need to understand when you can override safely and when you can’t without doing extra work. Simon Willison has an interesting article on this: Styling an HTML dialog modal to take the full height of the viewport.

Limited Width and Centering

This example has lots of written content in it (a bunch of s) so it’s best practice to limit the width to a readable line length. When that’s the intent, it’s nice to use the ch unit as it maps roughly to number of characters, which is what we’re trying to limit.

dialog {
  ...

  inline-size: min(50ch, 90dvi);
  margin-inline: auto;
}Code language: CSS (css)

Fifty characters of width is providing good readability here, but it’s possible that the current screen is actually narrower than that, hence the min() function assuring that the width will never be wider than 90% of the viewport. I’m not sure if our fancy dancy “dynamic viewport units in the inline direction” is buying us anything here, but it balances the usage with where we were using dvb).

Screenshot 2025 04 15 at 3.25.18%E2%80%AFPM 1
Screenshot 2025 04 15 at 3.25.27%E2%80%AFPM

Modal vs Non Modal (and the open attribute)

This seems like a pretty important distinction to know about:

  • “Modal” (dialog.showModal()) means interrupt everything else, this dialog needs to be dealt with immediately.
    • The ESC key automatically works to close it.
    • Focus is put on the first focusable element within the dialog
    • Focus is trapped within the dialog
  • “Non Modal” (dialog.show()) means the dialog is just there, but doesn’t require exclusive or immediate action.
    • None of those other things above happen. You likely want to bind the ESC key yourself anyway.
    • When you use the open attribute (useful when working on them!) like the dialog is open non-modally.

In our example, where a person needs to accept-or-not the Terms & Conditions, it’s likely modal is the better approach. That way what the person is trying to do can only continue if they accept or take a different path if they do not. This choice is likely required to know what to do next.

A non-modal dialog implementation might be something like a “site navigation drawer” where some of the attributes of using a modal is desirable (e.g. the hide/show behavior) but focus trapping is not required or even desirable.

Here’s a video of focus trapping at work with the modal state. Notice the “focusable element” (an anchor link) never gets focus, because it’s not within the

.

No Invokers?

There is no way to show a dialog in the modal state from HTML alone. Not by default nor by button click. The .showModal() function must be used. For that reason, it may feel like it would be an advantage to try to use popovers instead, which do have HTML-only controls, aka “invokers”. Unfortunately, if you go with a popover, you don’t get the focus trapping that you can get with a modal dialog, which is ultra useful (see video above).

To bone up: What’s the Difference Between HTML’s Dialog Element and Popovers?

Maybe someday we can get something like:



<button dialogTarget="my-dialog">
  Toggle Dialog in Non-Modal State
button>

<button dialogTargetModal="my-dialog">
  Toggle Dialog in Modal State
button>Code language: HTML, XML (xml)

Careful with the display value

The reason that

is invisible by default is simply that default styles render it with display: none;. That is precariously easy to override. In fact, in this very demo I was playing with, I wanted to use display: flex; on the dialog to have the header/content/footer look where the content is flex: 1; to push the header and footer away and take up the remaining space. But you’ll have problems like this:


dialog {
  display: flex;
}Code language: CSS (css)

It’s probably most resilient to just not mess with the display value of dialogs, instead using some internal wrapper element instead. But I’m a gamblin’ man apparently so I did:

dialog {
  &[open] {
    display: flex;
  }
}

Trimming margin can come anytime now

Any time I slap a bunch of elements into a container (read: doing web design) I’m reminded that the block-direction margins are kind of annoying in that context. The last item, particularly if it’s content, will likely have margin at the end that pushes further than you want it away from the container, or the start, or both.

It leads to this kind of thing:

.container {
  :first-child {
    margin-block-start: 0;
  }
  :last-child {
    margin-block-end: 0;
  }
}

When instead we could be living in the future like:

.container {
  margin-trim: block;
}Code language: CSS (css)

I once said this and I’m sticking to it:

If you add padding in the main flow direction of an element, adding margin-trim in that same direction.

Right aligned buttons deux façons

I had item-flow on my brain when I was tinkering with this and thinking about how flow directions can be reversed, which is something I don’t think about or use very much. For some reason when I needed to right-align those buttons for “Accept” and “Close”, my fingers went for:

dialog {
  > footer {
    display: flex;
    flex-direction: row-reverse;
  }
}

I’m not going to recommend that, as it changes the tabbing order awkwardly for no great reason. You should probably just do:

dialog {
  > footer {
    display: flex;
    justify-content: end;
  }
}

But, ya know, always nice to have options. You could also not even bother with flex and do text-align: end or even old school float: right the buttons.

Autofocus

In reading over the MDN for dialogs, this stood out to me as something I didn’t know:

The autofocus attribute should be added to the element the user is expected to interact with immediately upon opening a modal dialog. If no other element involves more immediate interaction, it is recommended to add autofocus to the close button inside the dialog, or the dialog itself if the user is expected to click/activate it to dismiss.

They didn’t mince words there and it makes sense to me, so I put it on the “Accept” button as that seems like the most likely user action.

<dialog>
  ...
  <footer>
    ...
    <button autofocus>Acceptbutton>
  footer>
dialog>Code language: HTML, XML (xml)

Feel free to peak at the demo to see a few other thing like color modes and a backdrop. Sometimes fairly simple looking HTML elements have quite a bit of detail to implementation!

Share.
Leave A Reply