Introduction
For more than a decade, building modals on the web meant reimplementing infrastructure:
- focus traps
- scroll lock
- ARIA wiring
- z-index battles
- extreme cases of accessibility
Frameworks solved it with layers of abstraction, but the browser didn't have a native solution.
That changed.
The <dialogue>
The element is no longer an experimental curiosity. As of 2022-2023, it has become a fully viable modal primitive for all browsers, complete with focus management, top-layer rendering, background handling, and built-in accessibility.
This article explains what <dialog>
really does it, how is it different from show()
and showModal()
, how it compares to <details>
and the Popover API
and when you should (and shouldn't) replace your custom modals with it.
How to implement
<dialog id="myDialog">
<p>Hello world</p>
<button onclick="myDialog.close()">Close</button>
</dialogue>
<button onclick="myDialog.showModal()">Open</button>
JavaScript API
The true power of <dialogue>
It's in your methods:
.showModal()
- sets
open
attribute: promote dialogue to the "upper layer"
- create
::backdrop
- applies
inert
to the rest of the document: exposes the dialog box as
aria-modal="true"
to assistive technology: move the focus
- register the ESC listener
The "top layer" is not only used in dialog boxes, but also in the full screen API, pop-ups, and context menus. It is located above the normal stacking context. That's why the z-index doesn't affect modal dialogs at all: they are rendered on a separate top layer above the normal stacking context.
.show()
- sets
open
attribute: keeps the dialog in the normal layout and in the stacking context
When you open the dialog with show()
there is no inert
, backdrop
, focus trap
, aria-modal
, only the dialog visible.
.close()
- Close the dialogue.
background style
When opened with .showModal()
, the browser creates a background as a special pseudo-element. You design it like this:
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
Common errors when using <dialog>
Although <dialogue>
simplifies modal behavior, there are some bugs that developers frequently run into.
Using open
Instead of showModal()
Settings <open dialog>
o dialog.open = true
it doesn't make the dialogue modal. It just makes it visible.
There is no focus trap, no background, no inert background, and no top layer behavior.
If you need modal behavior, always use dialog.showModal()
.
Forget to label the dialogue
A dialog box without an accessible name is confusing to screen reader users.
Always provide one of the <dialog aria-labelledby="titleId">
o <dialog aria-label="Delete element">
.
If the dialog has no title or label, it will simply be announced as "dialog", which provides no context.
Incorrectly animate open attribute
A closed dialog box is effectively displayed: none
. That means that transitions often don't run when the dialog is first opened.
To animate correctly, you can use @starting-style
(modern CSS), or apply a class on the next animation frame after calling showModal()
.
Waiting for show()
behave like showModal()
show()
it just makes the dialog visible. It doesn't move it to the top layer, it doesn't catch focus, it doesn't create a background, it doesn't make the background inert.
.
If you need to block the interaction, use showModal()
.
Open multiple modal dialogs
Only one modal dialog can be opened at a time. Calling showModal()
in a second dialog while one is already open will throw an error. If you need cumulative interactions, close the current dialog box before opening another one.
Reimplement what the browser already does
Some developers still add their own focus trap, add role="dialog"
manually, add aria-modal="true"
manually, lock the body movement manually.
When showModal() is used
, this is unnecessary and may even conflict with native behavior. Let the platform take care of it.
Accessibility Notes
By default a showModal()
dialogue
- behaves like
role="dialogue"
- has