Lessons Learned from Recreating a Styled Dialog

Chris Coyier Chris Coyier on

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 <dialog> because apparently I’m both of those types of nerd.

It’s a <dialog>

This thing came up on top of other content, so that alone makes it appropriate for the HTML <dialog> element. We’ll use that.

No Taller than Viewport

It’s not absolutely required that the entire <dialog> 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 <dialog> 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.

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 <p>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).

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 <dialog open> 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 <dialog>.

No Invokers? Yes Invokers!

There is no way to show a dialog in the modal state from HTML alone.

Welllll, the above isn’t strictly true anymore as I learned from Curtis Wilcox in the comments. We can actually use the popover syntax to make a button in HTML alone that will open the dialog. That will (sadly) only open the dialog in the non-modal state, but at least it’s a toggle without JavaScript! The good news is that the Invoker Commands API is actually all over this. It’s used like this:

<dialog id="my-dialog">
  ...
</dialog>

<!-- 
  popovertarget is the fallback

  command attributes are the new school,
  which open in a modal state!
-->
<button
  popovertarget="my-dialog"

  command="show-modal"
  commandfor="my-dialog"
>
  Open Modal
</button>

<button
  popovertarget="my-dialog"
  popovertargetaction="hide"

  commandfor="my-dialog"
  command="close"
>
  Close
</button>Code language: HTML, XML (xml)

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

Careful with the display value

The reason that <dialog> 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:

/* Oops, dialog is always open */
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>Accept</button>
  </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!

Looking for a complete course on getting into web development?

Frontend Masters logo

We have a complete intro course to web development by renowned developer Brian Holt from Microsoft. You'll learn how to be a successful coder knowing everything from practical HTML and CSS to modern JavaScript to Git and basic back-end development.

7-Day Free Trial

3 responses to “Lessons Learned from Recreating a Styled Dialog”

  1. Curtis Wilcox says:

    “There is no way to show a dialog in the modal state from HTML alone.”

    There is and it’s freakin’ awesome!

    (Omitting angle brackets because I don’t know what this comment field supports)

    button command="show-modal" commandfor="mydialog"

    Like popovertarget, commandfor takes the id of the dialog element it’s meant for.

    A few declarative command values have been defined, show-modal triggers mydialog.showModal(). It’s expected that more commands will be added to specs in the future but you can also make up your own commands to be handled by JavaScript.

    The Invoker Commands API has shipped in Chromium, it’s in Safari Technology Preview, and in Firefox Nightly. I’m hoping that it will ship in all of them (be “Baseline Newly Available”) before 2026.

    In the meantime, you can put popovertarget="mydialog" on the same button, command supersedes it in browsers that support it. A dialog element opened by popover won’t be modal but at least it will be open. You can also feature detect support for command (document.createElement('button').commandForElement returns null in browsers that support it, undefined in others) and open the modal dialog using JavaScript.

    Same modal dialog with command plus non-modal popover fallback, no JavaScript:

    https://codepen.io/ccwilcox/pen/yyyLEzN

    https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API

  2. I have good news for you, HTML-only opening is coming to dialog! There’s commandfor which contains the id of your dialog, and command which is show-modal to show and close to close again. More here: https://developer.chrome.com/blog/command-and-commandfor

Leave a Reply to Curtis Wilcox Cancel reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $363,806 contributed to date.