Lessons Learned from Recreating a Styled Dialog
Lessons Learned from Recreating a Styled Dialog êŽë š
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;
}
That will limit the height to essentially 90% of the viewport height (the dv
b 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.

(source)
Note
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;
}
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>
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;
}
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;
}
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 Dialog element developer.mozilla.org
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>
Feel free to peak at the demo (chriscoyier
) 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!