HTML Dialog Element: How to Use and Test Native Dialogs
Build native HTML dialogs with the right modal behavior, focus, forms, and dismissal. Follow runnable examples and a practical browser testing checklist.
Use the native <dialog> element and call showModal() when the interaction must block the rest of the page. Call show() for a non-modal dialog that leaves the page usable. Close dialogs with close(), requestClose(), or a form using method="dialog"; do not remove the open attribute by hand. The examples below cover focus, keyboard dismissal, form results, and how to test both modes.
1. Build a modal dialog
This complete example opens a confirmation dialog, puts initial focus on the safe choice, and reads the selected button’s value after the dialog closes.
<button id="open-confirm" type="button">Delete item</button>
<dialog id="confirm-dialog" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete this item?</h2>
<p>This action cannot be undone.</p>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<script>
const dialog = document.querySelector("#confirm-dialog");
const opener = document.querySelector("#open-confirm");
opener.addEventListener("click", () => {
dialog.showModal();
});
dialog.addEventListener("close", () => {
if (dialog.returnValue === "confirm") {
// Perform the confirmed action here.
console.log("Confirmed");
}
});
</script>
Use a unique accessible name, such as the heading referenced by aria-labelledby, and keep a visible button for the decision or dismissal. Pick the initial focus target for the task: in this example, Cancel is focused so the destructive action is not the default. MDN recommends autofocus on the element that should receive immediate interaction. For complex content, deliberately focusing the dialog itself may be appropriate. Do not put tabindex on the <dialog> element. [MDN: The Dialog element]
2. Choose modal or non-modal behavior
| Method | Use when | What happens around it |
|---|---|---|
showModal() |
The user must address an interruption or decision before continuing. | The dialog enters the top layer, gets a backdrop, and the rest of its containing document becomes inert. |
show() |
The dialog is a supplementary panel or tool that can coexist with page interaction. | The surrounding document remains interactive; it is not modal. |
Do not choose modal behavior just because the UI looks like a box over the page. Ask whether the task should prevent interaction with the rest of the document. Test the two paths separately: an open dialog does not by itself tell you whether the page behind it is blocked. A modal opened inside an iframe blocks only that iframe’s document, not the embedding page. [HTML Standard: The dialog element]
const dialog = document.querySelector("#help-dialog");
// Modal: blocks the containing document until dismissed.
dialog.showModal();
// Non-modal: leaves the document usable.
// Call this on a closed dialog, or close it before changing modes.
dialog.close();
dialog.show();
Style a modal backdrop using the ::backdrop pseudo-element:
dialog::backdrop {
background: rgb(0 0 0 / 55%);
}
dialog {
max-width: min(36rem, calc(100vw - 2rem));
border: 0;
border-radius: 0.75rem;
padding: 1.5rem;
}
3. Close the dialog and handle results
Explicit close with a dialog form
A form with method="dialog" closes the dialog on successful submission without sending form data to a server. The activated submit button’s value becomes returnValue. This is useful for a small choice such as confirm versus cancel.
<dialog id="choice" aria-labelledby="choice-title">
<h2 id="choice-title">Save changes?</h2>
<form method="dialog">
<button value="discard">Discard</button>
<button value="save" autofocus>Save</button>
</form>
</dialog>
<script>
const choice = document.querySelector("#choice");
choice.addEventListener("close", () => {
switch (choice.returnValue) {
case "save":
saveChanges();
break;
case "discard":
discardChanges();
break;
}
});
choice.showModal();
</script>
For a close button that does not submit a form, call close(). You can optionally pass a result string, which is exposed as returnValue:
dialog.close("dismissed");
Close requests and Escape
A modal opened with showModal() supports Escape dismissal by default. The cancel event represents a close request; it fires before the dialog closes and can be canceled. If a close request must be refused until the user completes a task, prevent the event deliberately and provide an explanation and another clear way forward. The close event fires after closure has happened.
dialog.addEventListener("cancel", (event) => {
// Optional policy: keep the dialog open while a required action is pending.
if (hasUnsavedCriticalWork()) {
event.preventDefault();
showInlineMessage("Complete or discard the pending change first.");
}
});
dialog.addEventListener("close", () => {
console.log("Closed with:", dialog.returnValue);
});
requestClose() follows the close-request path, so cancel is fired first and can prevent closure. Use it when code should behave like a user close request. Use close() when code should close directly. Changing the open attribute manually is not a substitute: it can skip the close event and leave modal state inconsistent. [HTML Standard: The dialog element] [MDN: HTMLDialogElement]
4. Test native dialog behavior
Test behavior, not just appearance. Run this checklist for each supported browser and embedded WebView:
- Activate the opener and confirm that the dialog opens using the intended method.
- For
showModal(), try to activate a control behind the dialog. The containing document should be inert while the modal is open. - Verify focus starts at the intended control, including the effect of
autofocus, and that keyboard users can reach the dialog’s controls. - Activate the explicit close or decision control. Confirm the dialog closes and the
closehandler runs. - Press Escape. Confirm the
cancelevent path runs and the dialog closes unless the event is prevented. - If your code prevents
cancel, verify that the dialog remains open and explains what the user should do. - Submit every
method="dialog"button and check the resultingreturnValue. - Test
show()independently and verify that background page controls remain usable. - Repeat in the actual browsers and WebViews your product supports, including the iframe context if the dialog is embedded.
For repeatable manual checks, log the events and result while developing:
for (const type of ["cancel", "close"]) {
dialog.addEventListener(type, () => {
console.log(type, { open: dialog.open, returnValue: dialog.returnValue });
});
}
The checklist describes expected behavior from the platform documentation; it is a test plan, not a claim that a particular browser run has been performed.
5. Browser support and compatibility
MDN describes showModal() as widely available across browsers since March 2022. The HTML Standard compatibility notes list Firefox 98+, Safari 15.4+, Chrome 37+, and Edge 79+ for core dialog methods, with Internet Explorer unsupported. Treat these as source-reported minimums, then verify the exact methods and features in the browser and WebView versions you support. Support for a core method does not prove that every related behavior or newer method is available in every embedded runtime. [MDN: The Dialog element] [HTML Standard: The dialog element]
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
showModal is not a function |
The element lookup returned something other than a dialog, the code ran before the element existed, or the runtime lacks support. | Confirm the selector and that the markup uses <dialog>; run after the DOM exists; check the target browser or WebView version. |
| The dialog is visible but the page behind it still works | It was opened with show() or only the open attribute was set. |
Call showModal() when background interaction must be blocked. |
| Escape closes a dialog that should stay open | The close-request event was not handled. | Listen for cancel and call preventDefault() only when there is a clear reason to refuse dismissal. Explain the requirement to the user. |
| Escape does nothing | A cancel listener may be preventing the event, or the dialog may not be in modal state. |
Inspect event handlers and confirm the dialog was opened with showModal(). |
| The result is empty or unexpected | The submit button has no intended value, a different button was activated, or code reads the result before closure. |
Give each choice an explicit value and handle it in the close event. |
The close handler does not run after removing open |
Removing the attribute manually bypasses the dialog close method and its event path. | Use close(), requestClose(), or a method="dialog" submission. |
| Focus starts in the wrong place | No deliberate initial-focus choice was made, or autofocus points at an unsuitable control. | Place autofocus on the appropriate interactive element, or focus suitable dialog content for a complex interaction. Verify with keyboard navigation. |
| Behavior differs in an embedded page | A modal affects only its containing document; browser or WebView versions may also differ. | Test the embedding context and supported runtime matrix directly. |
7. Performance, reliability, and implementation notes
The native dialog supplies browser-managed modal mechanics such as the top layer and inert background for showModal(). Keep the dialog content and event handlers proportionate to the interaction: defer expensive content until it is needed, avoid doing network work in a close handler unless the user’s choice requires it, and make async actions show their own progress and failure state. These are application design practices; the platform documentation does not provide performance benchmarks for a particular dialog.
For reliability, use one clear state path: open with the appropriate method, close through dialog APIs or a dialog form, and process the result after the close event. Test Escape, explicit buttons, repeated open/close cycles, and failure paths for actions triggered by the result. Native behavior reduces the amount of custom modal plumbing, but it does not decide your initial focus, content, action safety, or product-specific dismissal policy.
8. Or skip the browser setup
If your task is to capture a page that contains a dialog, ScreenshotNeo can return a screenshot or PDF from one API request. It is a website screenshot API and MCP server for developers. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month.
9. FAQ
Should every dialog have a backdrop?
No. A modal opened with showModal() has a backdrop pseudo-element that you can style. A non-modal dialog opened with show() is not modal.
Does method="dialog" send the form to a server?
No. It closes the dialog on successful submission and makes the activated button’s value available as returnValue.
Can I prevent Escape from closing a modal?
Yes. Prevent the dialog’s cancel event. Do this only for a deliberate interaction requirement, and give the user a clear alternative way to proceed or close.
Can a dialog be modal inside an iframe?
Yes. Its modal behavior makes the iframe’s document inert; it does not make the embedding document inert.


