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.

<dialog>
It’s a 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 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.

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


open
attribute)
Modal vs Non Modal (and the 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?
display
value
Careful with the 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;
}
}
margin
can come anytime now
Trimming 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, addingmargin-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 addautofocus
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!
“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
triggersmydialog.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 forcommand
(document.createElement('button').commandForElement
returnsnull
in browsers that support it,undefined
in others) and open the modal dialog using JavaScript.Same modal dialog with
command
plus non-modalpopover
fallback, no JavaScript:https://codepen.io/ccwilcox/pen/yyyLEzN
https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API
Huge. Love it. Will update post.
I have good news for you, HTML-only opening is coming to dialog! There’s
commandfor
which contains the id of your dialog, andcommand
which isshow-modal
to show andclose
to close again. More here: https://developer.chrome.com/blog/command-and-commandfor