Introduction
For more than a decade, building modals on the web meant re-implementing infrastructure:
- focus traps
- scroll locking
- ARIA wiring
- z-index battles
- accessibility edge cases
Frameworks solved it with layers of abstraction, but the browser had no native solution.
That changed.
The <dialog> element is no longer an experimental curiosity. Since 2022–2023, it has become a fully viable, cross-browser modal primitive — complete with focus management, top-layer rendering, backdrop handling, and built-in accessibility.
This article explains what <dialog> really does, how it differs 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>
</dialog>
<button onclick="myDialog.showModal()">Open</button>
JavaScript API
The real power of <dialog> is in its methods:
.showModal()
- sets
openattribute - promotes dialog into “top layer”
- creates
::backdrop - applies
inertto rest of document - exposes the dialog as
aria-modal="true"to assistive technology - moves focus
- registers ESC listener
The “top layer” is not just used by dialog, but also by Fullscreen API, Popovers, and Context menus. It sits above normal stacking context. That’s why z-index does not affect modal dialogs at all: they are rendered in a separate top layer above the normal stacking context.
.show()
- sets
openattribute - keeps the dialog in normal layout and stacking context
When you open the dialog with show() there is no inert, backdrop, focus trap, aria-modal, just the visible dialog.
.close()
- Closes dialog.
Backdrop Styling
When opened with .showModal(), the browser creates a backdrop as a special pseudo-element. You style it like this:
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
Common Mistakes When Using <dialog>
Even though <dialog> simplifies modal behavior, there are a few pitfalls that developers frequently run into.
Using open Instead of showModal()
Setting <dialog open> or dialog.open = true does not make the dialog modal. It only makes it visible.
There is no focus trap, no backdrop, no inert background, no top-layer behavior.
If you need modal behavior, always use dialog.showModal().
Forgetting to Label the Dialog
A dialog without an accessible name is confusing for screen reader users.
Always provide one of <dialog aria-labelledby="titleId"> or <dialog aria-label="Delete item">.
If the dialog has no heading and no label, it will simply be announced as “dialog”, which provides no context.
Animating the open Attribute Incorrectly
A closed dialog is effectively display: none. That means transitions often don’t run when the dialog first opens.
To animate correctly, you either use @starting-style (modern CSS), or apply a class in the next animation frame after calling showModal().
Expecting show() to behave like showModal()
show() only makes the dialog visible. It does not move it to the top layer, trap focus, create a backdrop, make the background inert.
If you need blocking interaction, use showModal().
Opening Multiple Modal Dialogs
Only one modal dialog can be open at a time. Calling showModal() on a second dialog while one is already open will throw an error. If you need stacked interactions, close the current dialog before opening another.
Re-Implementing What the Browser Already Does
Some developers still add their own focus trap, add role="dialog" manually, add aria-modal="true" manually, lock body scroll manually.
When using showModal(), this is unnecessary and can even conflict with native behavior. Let the platform handle it.
Accessibility Notes
By default a showModal() dialog
- behaves like
role="dialog" - has
aria-modal="true"set
You still need accessible labeling. Screen readers need to know what the dialog is about. So this is good
<dialog aria-labelledby="dialogTitle">
<h2 id="dialogTitle">Delete item</h2>
...
</dialog>
or
<dialog aria-labelledby="confirm-title" aria-describedby="confirm-desc">
<h2 id="confirm-title">Delete file?</h2>
<p id="confirm-desc">This cannot be undone.</p>
...
</dialog>
or
<dialog aria-label="Delete item">
Note: If your dialog contains no heading at all, you MUST provide a label via aria-label. Because a dialog without a name is bad for screen readers.
What inert actually does
When you call dialog.showModal() the browser internally makes everything outside the dialog inert. inert is an HTML attribute that means this subtree is not interactive and not focusable. It does three things
- removes elements from the tab order
- blocks pointer events
- removes the subtree from the accessibility tree and prevents user interaction
What's the difference to
-
display: none: removes from layout and accessibility tree -
visibility: hidden: hides visually but still affects layout -
inert: keeps layout visible but disables interaction + accessibility
That’s why background content is still visible but inaccessible.
How Focus Trapping Works Without JS
With <dialog> when opened then
- the browser moves focus inside the dialog
-
Tabcycles only inside dialog -
Shift+Tabalso cycles inside -
ESCtriggers cancel
On close the previous focus is restored. No JS required.
Tip: Initial focus
Browser will focus on the first focusable element inside dialog. If none exist, it may focus the dialog itself. Best practice is to set the focus on an element in the dialog, e.g. <button autofocus>Cancel</button>.
All the work you do not need to do
Before <dialog>, modals required:
- focus trap logic
- scroll locking
- ARIA wiring
- z-index management
- ESC handling
- backdrop click logic
Now: dialog.showModal().
Should you replace all my modals with <dialog>?
Not automatically. But the new modals <dialog> is often the cleanest option if it fits your UX.
How new really is the <dialog> HTML element?
Although was introduced around 2012 and implemented in Chrome in 2014, it wasn’t fully viable cross-browser until 2022–2023 when Safari and Firefox shipped consistent modal behavior, ::backdrop, and inert. That alignment marked its real turning point.
But it still feels new now, because around 2022–2024 Popover API shipped, Inert shipped, Anchor positioning started appearing, Container queries became stable. So the platform shifted toward native UI primitives. <dialog> suddenly made sense again.
Before <dialog>, modals were accessibility nightmares, focus-trap hacks, scroll-lock hacks, z-index wars, screen-reader inconsistencies. The spec authors wanted a built-in, accessibility-safe modal primitive. It just took years to reach consistent implementation.
So which of my custom modals should I replace straight away?
When <dialog> is a great replacement
- You want a standard centered modal
- You want correct focus trap + restore with minimal code
- You don’t need fancy portal stacking logic
- You’re happy with “platform behavior” (ESC, backdrop, top layer)
When you might not want <dialog> everywhere
- Your modal is actually a drawer / side panel (sometimes better as a “region” or popover, depending)
- You rely on a UI library modal that already handles everything well (e.g. AntD Modal)
- You need very custom stacking / nested modals / toasts interacting with modals
- You need full control over close timing/animations (still possible, but a bit more work)
A pragmatic recommendation
Migrate gradually. Replace simple confirm/alert/small-form modals first. Keep complex stacked/drawer-style modals where a framework abstraction makes more sense.
Last not least, what's the alternative? A comparison of <dialog> vs <details> vs popover
<details> (2010s)
Purpose: simple disclosure / expand-collapse.
Behavior: built-in toggle, open attribute, keyboard support
Adoption story: adoption was slow, but it was eventually widely accepted and is now commonly used
Pattern: declarative + small built-in state machine.
<dialog> (viable 2022)
Purpose: modal / interaction interruption
Behavior: modal state machine, focus trap, inert background, backdrop, top layer
Adoption story: long stagnation, suddenly viable once Safari + Firefox implemented fully, but still catching up in ecosystem mindset
Pattern: semantic + heavy interaction behavior.
popover (2023+)
Purpose: lightweight floating UI
Behavior: top layer, light dismiss, ESC handling, no focus trap
Adoption: very new, likely to replace many custom dropdown libraries
Pattern: attribute-based primitive, not a new HTML element.
When should you use dialog and when is it a popup?
Use a modal <dialog> (showModal()) when you need to stop the user from continuing until they respond. Typical signals:
- Decision required: confirm / cancel (“Delete snippet?”, “Discard changes?”)
- Blocking error: “Session expired — sign in again”
- Wizard / required form: must finish or explicitly dismiss
- Critical info where continuing without acknowledging is risky
- You need focus trap + inert background (keyboard can’t escape to the page)
Examples: confirm delete, unsaved changes prompt, payment / security step, required “choose one option to continue”.
Use a non-modal dialog / popup when it’s supplementary, doesn’t require a decision, and the user should be able to keep working. This includes:
- Tooltips (“This field must be unique”)
- Info popovers (“What’s this?” help bubble)
- Context menus (right-click style menu)
- Inline pickers (color picker, date picker, emoji picker)
- Autocomplete dropdowns
- Toast notifications (not a dialog at all)
Conclusion
<dialog> is not just another HTML element. It represents a shift in the web platform: moving interaction infrastructure from JavaScript frameworks into the browser itself.
For years, modal behavior required custom state machines. Today, a single call to dialog.showModal() gives you:
- Focus trapping
- Background inerting
- Backdrop rendering
- Escape handling
- Top-layer stacking
- Accessible semantics
That doesn’t mean you should rewrite every modal overnight. But it does mean that for simple, blocking interactions, the platform now provides a robust, accessible default.
Just as <details> simplified disclosure widgets, and popover is simplifying floating UI, <dialog> simplifies modal interaction.
The web platform is catching up with the patterns we’ve been re-implementing for years.
And this time, it’s doing it right.
Top comments (1)
Lovely work. My only tweak would be to add
htmlandjsto your code embeds, to add the highlighting.