The dialog Element: Native Modals
Modals with focus trapping, backdrop, Escape handling and inertness supplied by the browser. A component that used to need a library is now one element.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
A modal dialog is one of the hardest components to build correctly. Done by hand it needs focus trapping, an inert background, Escape handling, a backdrop, scroll locking, focus restoration and correct ARIA roles - and most hand built versions get at least two of those wrong.
The dialog element supplies all of it.
Syntax
<button type="button" id="open">Apply now</button>
<dialog id="apply-dialog">
<h2>Apply for the design course</h2>
<p>Applications close on 30 June.</p>
<form method="dialog">
<button type="submit" value="cancel">Close</button>
<button type="submit" value="continue">Continue</button>
</form>
</dialog>const dialog = document.getElementById("apply-dialog");
document.getElementById("open").addEventListener("click", () => {
dialog.showModal();
});
dialog.addEventListener("close", () => {
console.log(dialog.returnValue); // "cancel" or "continue"
});showModal versus show
showModal() | show() | |
|---|---|---|
| Rest of the page | Inert - not clickable, not focusable, not read | Fully usable |
| Backdrop | Yes, ::backdrop | No |
| Escape closes it | Yes | No |
| Focus trapped inside | Yes | No |
| Layer | The browser top layer, above everything | Normal stacking |
showModal() is what modal means and is what you want almost always. show() creates a non modal dialog, closer to a floating panel.
Do not use the open attribute to display a modal. It shows the dialog without any of the modal behaviour - no backdrop, no focus management, no inertness.
What the browser handles for you
- Focus moves in on open, to the first focusable element or to the element carrying
autofocus. - Focus is trapped: Tab cycles within the dialog and cannot escape.
- The background is inert: nothing behind it can be clicked, focused or read by a screen reader.
- Escape closes it, firing a
cancelevent first. - Focus is restored to the element that opened it.
- It renders in the top layer, above every stacking context, so no
z-indexfight is possible.
That list is the entire reason to use this element.
method="dialog"
A form inside a dialog with method="dialog" closes the dialog on submit instead of sending anything to a server, and sets returnValue to the pressed button value.
<dialog id="confirm">
<h2>Delete this application?</h2>
<p>This cannot be undone.</p>
<form method="dialog">
<button type="submit" value="no" autofocus>Keep it</button>
<button type="submit" value="yes">Delete</button>
</form>
</dialog>confirm.addEventListener("close", () => {
if (confirm.returnValue === "yes") deleteApplication();
});Note the autofocus on the safe option. For a destructive confirmation, focus should land on the action that does nothing.
Styling
dialog {
border: none;
border-radius: 12px;
padding: 1.5rem;
max-width: 32rem;
width: calc(100% - 2rem);
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
}
dialog::backdrop {
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(2px);
}
/* a closed dialog is display:none, so animate carefully */
dialog[open] {
animation: dialog-in 160ms ease-out;
}
@keyframes dialog-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
dialog[open] { animation: none; }
}::backdrop is a real pseudo element and cannot be reached any other way. It only exists for a dialog opened with showModal().
Closing on a backdrop click
Not built in, and worth adding. The trick is that a click on the backdrop reports the dialog itself as the target, because the backdrop belongs to it:
dialog.addEventListener("click", (event) => {
if (event.target === dialog) dialog.close();
});For that to work, the dialog needs an inner wrapper holding the content, so clicks on the content do not match.
Accessibility notes
- The element already has
role="dialog". Do not add it. - Give it an accessible name: a heading plus
aria-labelledby, or anaria-label. - Always provide a visible close control. Escape alone is not enough on touch.
- Put
autofocuson the least destructive control. - Do not open a modal on page load. It is disorienting and usually dismissed.
<dialog id="apply" aria-labelledby="apply-title">
<h2 id="apply-title">Apply for the design course</h2>
<button type="button" class="close" aria-label="Close dialog"></button>
</dialog>Important rules
- A closed
dialogisdisplay: none, so its contents are unreachable. showModal()throws if the dialog is already open.close()can be given a value that becomesreturnValue.- The
cancelevent fires on Escape and can be prevented. - Body scroll is not locked automatically; add
overflow: hiddenonhtmlif the background scrolling bothers you. - Only one modal dialog can be in the top layer at a time in practice.
Common mistakes
- Using the
openattribute and losing every modal behaviour. - Adding
role="dialog"redundantly. - No accessible name, so it is announced as just dialog.
- No visible close button.
- Focusing the destructive option by default.
- Trying to animate a dialog without accounting for
display: none. - Building a modal from a
divand reimplementing all of this by hand.
Best practices
- Always
showModal()for a modal. - Name the dialog with
aria-labelledbypointing at its heading. - Use
method="dialog"for confirmation forms. - Add backdrop click closing where it is appropriate.
- Respect
prefers-reduced-motion. - Never open a modal unprompted.
- Consider the
popoverattribute for lightweight non modal overlays.
Practice
- Build a confirmation dialog with two buttons and read
returnValueon close. - Open it with the
openattribute and then withshowModal(), and list every difference in behaviour. - Style
::backdropwith a blur and confirm it only appears for the modal form. - Tab through an open dialog and confirm focus cannot leave it, then confirm focus returns to the trigger on close.