HTML Dialog Element
A dialog is a small window that appears on top of the page and asks for the visitor's attention: "Delete this project?", "Sign in", "Choose a date". For years, developers built these with piles of div elements, scripts to trap the focus, and extra libraries.
The <dialog> element does most of that work for you. The browser handles the layer on top of the page, the dark backdrop, the Escape key, and keeping the focus inside. This lesson shows how to use it well.
What You Will Learnβ
- What
<dialog>is, and modal versus non-modal - Opening and closing with
showModal(),show(), andclose() - Forms with
method="dialog", and thereturnValue - Styling the dialog and its
::backdrop - Closing with Escape or a click outside
- Focus, accessibility, and alert dialogs
- Security, global notes, and common mistakes
The Basicsβ
- HTML Code
- Browser Output
<button type="button" id="open">Delete projectβ¦</button>
<dialog id="confirm" aria-labelledby="confirm-title">
<form method="dialog">
<h2 id="confirm-title">Delete this project?</h2>
<p>This removes all 14 files. You cannot undo it.</p>
<button value="cancel" autofocus>Keep it</button>
<button value="delete">Delete</button>
</form>
</dialog>
const dialog = document.getElementById("confirm");
document.getElementById("open").addEventListener("click", () => {
dialog.showModal();
});
dialog.addEventListener("close", () => {
console.log("Closed with:", dialog.returnValue);
});
returnValue: (nothing yet)
Try it above: open the dialog, press Escape, then open it again and press a button. Watch the log and the returnValue.
| Part | What it does |
|---|---|
<dialog> | The element. It is hidden until you open it |
showModal() | Opens it as a modal: on top of everything, with a backdrop, and the page behind becomes inert |
show() | Opens it as non-modal: the page stays usable |
close(value) | Closes it, and optionally sets the returnValue |
<form method="dialog"> | A form that closes the dialog instead of sending data, and sets returnValue to the pressed button's value |
::backdrop | The layer behind a modal dialog, styled with CSS |
close and cancel events | Fired after it closes, and when Escape is pressed |
Modal or Non-Modal?β
Think of a modal like a bank teller asking you to sign before they continue: nothing else happens until you deal with it. A non-modal dialog is more like a sticky note on the side of the screen: you can ignore it and carry on.
Modal (showModal()) | Non-modal (show()) | |
|---|---|---|
| Page behind | Inert: cannot be clicked, focused, or read by a screen reader | Fully usable |
| Backdrop | Yes (::backdrop) | No |
| Escape key | Closes it | Does nothing by default |
| Focus | Moves into the dialog, and returns to the opener on close | Not moved for you |
| Use for | Confirmations, sign-in, forms that need an answer | Side panels, chat widgets, helper notes |
Use a modal only when the visitor must decide before going on. Anything else is more polite as a non-modal element or a plain part of the page.
- HTML Code
- Browser Output
<button type="button" id="m">showModal()</button>
<button type="button" id="n">show()</button>
<dialog id="modal">
<h2>Modal dialog</h2>
<p>Everything behind me is inert.</p>
<button type="button" data-close>Close</button>
</dialog>
<dialog id="plain">
<h2>Non-modal dialog</h2>
<p>You can still use the page.</p>
<button type="button" data-close>Close</button>
</dialog>
document.getElementById("m").onclick = () => modal.showModal();
document.getElementById("n").onclick = () => plain.show();
document.querySelectorAll("[data-close]").forEach((button) => {
button.addEventListener("click", () => button.closest("dialog").close());
});
// Light dismiss: a click on the backdrop targets the dialog itself
modal.addEventListener("click", (event) => {
if (event.target === modal) modal.close();
});
Open one of the dialogs, then try to click or Tab to the other controls.
A click on the backdrop is reported as a click on the dialog element itself. So the dialog must have no padding, with an inner wrapper that holds the padding. Otherwise, a click on the padding looks like a click on the backdrop, and closes the dialog by mistake. The demo above does exactly that.
The open Attribute: Do Not Use It to Openβ
You can write <dialog open>, and it will show. But a dialog opened this way is non-modal, with no backdrop, no inert page, and no focus handling. It is fine for something you want visible from the start, but for a real pop-up, always call showModal() or show().
<!-- Visible straight away, but not a modal -->
<dialog open>Hello</dialog>
Never set open yourself to show a modal. Use the methods, and let the browser manage the attribute.
Forms Inside a Dialogβ
A form with method="dialog" does not send anything to a server. When submitted, it closes the dialog and sets dialog.returnValue to the value of the button that was pressed. This lets you collect an answer with no JavaScript at all.
<dialog id="pick">
<form method="dialog">
<h2 id="pick-title">Choose a plan</h2>
<p>
<label for="plan">Plan</label>
<select id="plan" name="plan">
<option>Free</option>
<option>Pro</option>
</select>
</p>
<button value="cancel" formnovalidate>Cancel</button>
<button value="ok">Choose</button>
</form>
</dialog>
pick.addEventListener("close", () => {
if (pick.returnValue === "ok") {
console.log("Chosen:", new FormData(pick.querySelector("form")).get("plan"));
}
});
Tips:
- Give a Cancel button
formnovalidate, so it works even when required fields are empty. - The form data is not sent anywhere. Read it yourself with
FormDatain theclosehandler, or use a normal form andfetch. - To collect data and send it to the server, use
method="post", and close the dialog yourself after success.
Closing a Dialogβ
| How | What happens |
|---|---|
dialog.close() | Closes it, and fires close |
A method="dialog" form submit | Closes it, with the button's value as returnValue |
| Escape (modal only) | Fires cancel, then closes it, and fires close |
| A click on the backdrop | Does nothing by default. Add your own handler, or use the newer closedby attribute (see below) |
Some points to know:
- The
cancelevent can be stopped withevent.preventDefault(), but do this rarely. Blocking Escape traps people. If you must, make sure the dialog has another visible way out. - The
closeevent fires every time, however the dialog closes, so it is the best place to run your follow-up code. - Newer browsers support a
closedbyattribute (closedby="any",closedby="closerequest", orclosedby="none") to control Escape and outside clicks without scripts. Support is still spreading, so check a compatibility table before relying on it, and keep the manual handler as a fallback.
Stylingβ
The dialog and its backdrop are styled with ordinary CSS:
dialog {
border: none;
border-radius: 12px;
padding: 0; /* padding goes on an inner wrapper */
width: min(90vw, 28rem);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}
dialog::backdrop {
background: rgba(15, 23, 42, 0.55);
}
/* Stop the page behind from scrolling while a modal is open */
body:has(dialog[open]:modal) {
overflow: hidden;
}
Notes:
- Do not set
displayondialogunconditionally (for exampledisplay: flex). It overrides the browser's hiding rule, so the dialog shows all the time. Styledialog[open]instead. - A modal appears in the browser's top layer, so
z-indexbattles do not matter, and no parent'soverflowortransformcan clip it. - Animating the open and close is possible with newer CSS features, but keep it short, and respect
prefers-reduced-motion. - On small screens, use
width: min(90vw, 28rem)andmax-height: 90vhwithoverflow: auto, so the dialog never gets bigger than the screen.
Focus and Accessibilityβ
showModal() gives you a lot for free:
- The page behind becomes inert: no clicks, no Tab, no screen reader access.
- The focus moves into the dialog, and returns to the button that opened it when it closes.
- Escape closes it.
- Screen readers announce it as a dialog.
You still need to do your part:
- Give it a name:
aria-labelledbypointing at the dialog's heading (oraria-labelif there is no visible title). - Describe it with
aria-describedbyfor short messages. - Choose where the focus starts with
autofocus. For a confirmation, put it on the safe button ("Keep it"), not on "Delete". If a dialog starts with a long text, focus the heading (withtabindex="-1") instead of the first button. - Provide a visible close button or Cancel, so nobody depends on knowing Escape.
- Make the buttons clear: "Delete project" and "Keep it", not "OK" and "Cancel", when the question is not obvious.
- Keep it short. A dialog is for one task. A whole page inside a dialog is a sign it should be a real page.
- Never open a modal automatically on page load (newsletter pop-ups, for example). It interrupts reading, and is a common reason visitors leave.
Alert dialogsβ
For urgent confirmations, such as destructive actions, add role="alertdialog" and aria-describedby that points at the message. Screen readers then announce the message straight away.
<dialog role="alertdialog" aria-labelledby="t" aria-describedby="d">
<h2 id="t">Delete this project?</h2>
<p id="d">This removes all 14 files. You cannot undo it.</p>
β¦
</dialog>
Use alertdialog sparingly. Most dialogs do not need it.
Dialog or Something Else?β
| You need | Use |
|---|---|
| A confirmation or a form that needs an answer first | <dialog> with showModal() |
| A small panel the visitor can ignore | Non-modal <dialog>, or a popover |
| A tooltip, menu, or hint | The popover attribute, not a dialog |
| Content the visitor opens and reads in place | <details> and <summary> |
| A short message after an action ("Saved") | A status message with role="status", not a dialog |
| A full flow (checkout, long forms) | A normal page |
The popover attribute is another built-in way to show floating content, better suited to menus and hints that do not need to block the page. Check browser support for your audience.
Security and Privacy Notesβ
- A dialog is not a security boundary. Its content is in the page source, even when it is closed. Never hide secrets, admin controls, or "protected" content inside a hidden dialog. Check permissions on the server.
- Confirmations are for people, not safety. A "Delete" dialog stops accidents, but the delete request still needs proper server-side authorisation and CSRF protection.
- Escape untrusted text that you put into a dialog (a file name, a user's comment). Use
textContent, notinnerHTML. - Beware of fake dialogs. Do not make a dialog that imitates a system or browser prompt, or a login box for another site. It is deceptive, and teaches people to trust look-alikes.
- Do not use a dialog for consent tricks. Make "Reject" as easy as "Accept" in cookie and privacy prompts.
- Do not collect more data than needed in a dialog form, and use the correct
autocompleteand input types, just as in any form.
Global Audience Notesβ
- Translate the title, message, and button labels, and allow for longer words in other languages. Do not give buttons a fixed width.
- Right-to-left languages: use logical properties (
margin-inline-start,text-align: start) so the buttons and close icon flip correctly. - Put the safest action first in the reading order, and explain what each button does, since "OK" and "Cancel" translate poorly and are easily confused.
- Do not depend on colour (a red "Delete") alone to show danger. Use clear words too.
- Consider slow connections and small phones. Keep dialogs light, avoid loading heavy content when they open, and test with large text sizes and on a narrow screen.
- Check support for newer features (
closedby, animated dialogs,popover) on the browsers your visitors use. The basic<dialog>is well supported in current browsers.
Common Mistakes to Avoidβ
- Using
opento show a modal instead ofshowModal(). - Forgetting
type="button"on buttons that should not submit a form. - No accessible name: no
aria-labelledbyoraria-label. - No visible way to close, relying on Escape alone.
- Putting padding on the dialog, so a click inside the padding closes it.
- Setting
display: flexorgridondialog, so it can never hide. - Blocking Escape with
preventDefault()with no other exit. - Starting the focus on the dangerous button.
- Opening a modal on page load.
- Vague buttons: "OK" and "Cancel" for a question that deserves clearer words.
- Treating a hidden dialog as secret.
- Using a modal for things that should be a page, a tooltip, or a message.
Practice Timeβ
Try these in your editor:
- Build a dialog with a title, a message, and a Close button, and open it with
showModal(). - Open the same dialog with
show()and compare how the page behaves. - Style the
::backdropwith a colour of your choice. - Add
aria-labelledbyand check it with a screen reader or the browser's accessibility panel. - Build a delete confirmation with
method="dialog"and two buttons withvalues, and log thereturnValue. - Add a click-outside handler, with the padding on an inner wrapper.
- Add
autofocusto the safe button, and test with the keyboard only. - Listen for the
cancelandcloseevents, and log which one fires for Escape. - Build a small form dialog ("Choose a plan"), and read the value with
FormDataonclose. - Bonus: add
body:has(dialog[open]:modal) { overflow: hidden; }, and check that the page stops scrolling behind a modal.
Quick Recapβ
Click a question to check your answer.
1. What is the difference between showModal() and show()?
showModal() opens a modal: it sits in the top layer with a backdrop, the page behind becomes inert, the focus moves inside, and Escape closes it. show() opens a non-modal dialog that leaves the page usable, with no backdrop and no Escape handling.
2. What does method="dialog" do on a form?
Submitting the form closes the dialog instead of sending data, and sets dialog.returnValue to the value of the button that was pressed.
3. Why not just add the open attribute to show a modal?
A dialog shown with open is non-modal: no backdrop, no inert page, and no focus handling. Use showModal() for a real modal.
4. How do you close a dialog when the visitor clicks outside it?
Add a click handler that closes it when event.target is the dialog itself, and put the padding on an inner wrapper so clicks inside the content are not mistaken for backdrop clicks. Newer browsers also offer the closedby attribute.
5. What must you add yourself for accessibility?
An accessible name (aria-labelledby or aria-label), a sensible starting focus (with autofocus, on the safe action), clear button labels, and a visible way to close it. Use role="alertdialog" for urgent confirmations.
6. Is a closed dialog a safe place to hide content?
No. Everything inside it is in the page source. Keep secrets and permission checks on the server.
Key Takeawaysβ
<dialog>is the built-in way to make pop-ups, with no libraries, and it handles the top layer, backdrop, Escape, and focus for you.- Use
showModal()when the visitor must answer first,show()for side panels, and never rely on theopenattribute to open a modal. <form method="dialog">closes the dialog and sets thereturnValue, so you can read the answer in thecloseevent.- Style the box and the
::backdropwith CSS, keep padding on an inner wrapper, and never forcedisplayon the dialog. - Give every dialog a name, a sensible first focus, and a visible way out. Use
alertdialogfor urgent confirmations. - A dialog is not a security boundary. Check permissions on the server and escape untrusted text.
- Do not open modals automatically, and plan for translation, right-to-left layouts, and small screens.
Next, put everything in this section together on one page, in Semantic Page Layout, or return to the Semantic HTML overview.