The <dialog> element represents a window separated from the rest of the page. It can be non-modal or modal, and those are meaningfully different behaviors.
The element
The open attribute displays a non-modal dialog without JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Open dialog</title>
</head>
<body>
<h1>Dashboard</h1>
<p>The page remains interactive around this non-modal dialog.</p>
<dialog open aria-labelledby="notice-title">
<h2 id="notice-title">Maintenance notice</h2>
<p>Reports may take longer to generate this afternoon.</p>
</dialog>
</body>
</html>This introduces the element, but hard-coding open is not the normal way to build an interactive modal. It does not place the dialog in the browser’s top layer, make the rest of the document inert, trap focus, or create a backdrop.
To Modal or Not to Modal…
A modal is intentionally disruptive. It prevents people from interacting with the rest of the page until they respond, so it should be reserved for a short, focused decision that cannot safely happen in the background.
- Use a modal dialog
- When the current task requires an immediate decision, confirmation, or small amount of information before it can continue—for example, confirming a destructive action.
- Use a non-modal dialog
- When supplemental tools or information can remain open while the user continues working—for example, upload status, find-and-replace controls, or a floating inspector.
- Do not use a dialog
- When the content is a complete workflow, belongs in the document flow, or does not require a response. Consider a dedicated page, inline form,
<details>, popover, status message, or notification instead.
Before choosing a modal, ask: What would go wrong if the user ignored this and continued using the page? If the answer is “nothing,” the interface probably should not be modal.
A non-modal dialog with show()
Use .show() when people still need to interact with the page outside the dialog.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Non-modal dialog</title>
<script src="/script.js" defer></script>
</head>
<body>
<button id="show">Show status</button>
<button>Another page action</button>
<dialog id="status" aria-labelledby="status-title">
<h2 id="status-title">Upload status</h2>
<p>Three files are still uploading.</p>
<button id="close">Close</button>
</dialog>
</body>
</html>const dialog = document.querySelector('#status');
document.querySelector('#show').addEventListener('click', () => {
dialog.show();
});
document.querySelector('#close').addEventListener('click', () => {
dialog.close();
});A modal dialog with showModal()
Use .showModal() when the user must respond before returning to the page. The browser moves the dialog into the top layer, makes content behind it inert, manages focus containment, and enables ::backdrop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modal dialog</title>
<script src="/script.js" defer></script>
</head>
<body>
<button id="delete">Delete project</button>
<dialog id="confirmation" aria-labelledby="confirmation-title">
<h2 id="confirmation-title">Delete this project?</h2>
<p>This action cannot be undone.</p>
<button id="cancel" autofocus>Cancel</button>
<button id="confirm">Delete project</button>
</dialog>
</body>
</html>const dialog = document.querySelector('#confirmation');
document.querySelector('#delete').addEventListener('click', () => {
dialog.showModal();
});
document.querySelector('#cancel').addEventListener('click', () => {
dialog.close('cancel');
});
document.querySelector('#confirm').addEventListener('click', () => {
dialog.close('confirm');
});
dialog.addEventListener('close', () => {
console.log(`Dialog result: ${dialog.returnValue}`);
});When it closes, focus normally returns to the element that opened it. Calling .close(value) stores the optional string on dialog.returnValue.
Modal is a behavior, not a look
Do not imitate a modal by adding open, position: fixed, and a high z-index. Use showModal() so the browser can provide top-layer placement, inert background content, focus containment, and modal semantics.
Forms can close dialogs
A form with method="dialog" closes its containing dialog without submitting data to a server. The activated submit button’s value becomes returnValue.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dialog form</title>
<script src="/script.js" defer></script>
</head>
<body>
<button id="choose">Choose a plan</button>
<output id="result">No plan selected.</output>
<dialog id="plans" aria-labelledby="plans-title">
<form method="dialog">
<h2 id="plans-title">Choose a plan</h2>
<p>Which plan fits your team?</p>
<button value="starter">Starter</button>
<button value="team">Team</button>
<button value="cancel">Cancel</button>
</form>
</dialog>
</body>
</html>const dialog = document.querySelector('#plans');
const result = document.querySelector('#result');
document.querySelector('#choose').addEventListener('click', () => {
dialog.showModal();
});
dialog.addEventListener('close', () => {
result.textContent = dialog.returnValue === 'cancel'
? 'Selection canceled.'
: `Selected plan: ${dialog.returnValue}`;
});Cancel and close events
A modal dialog can receive:
cancelwhen the browser receives a platform dismissal request, usually Escape. It is cancelable.closeafter the dialog has closed, regardless of how it closed.
const dialog = document.querySelector('dialog');
dialog.addEventListener('cancel', (event) => {
if (formHasUnsavedChanges) {
event.preventDefault();
}
});
dialog.addEventListener('close', () => {
console.log(dialog.returnValue);
});
Use preventDefault() sparingly. People expect Escape to dismiss a modal.
Light dismiss with closedby
The newer closedby attribute declares which user actions may close a dialog:
<dialog closedby="any">...</dialog>
none: only developer-provided controls close it.closerequest: platform close requests such as Escape may close it.any: also allows light dismiss by clicking outside it.
Treat closedby as a progressive enhancement and check browser support before relying on light dismiss as the only route out. Always provide a visible close or cancel button.
Styling the dialog and backdrop
Only after the behavior is correct do we add presentation.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css" />
<title>Styled dialog</title>
<script src="/script.js" defer></script>
</head>
<body>
<main>
<h1>Project settings</h1>
<button id="open">Rename project</button>
</main>
<dialog id="rename" aria-labelledby="rename-title">
<form method="dialog">
<h2 id="rename-title">Rename project</h2>
<label for="project-name">Project name</label>
<input id="project-name" name="project-name" value="Modern HTML" autofocus />
<div class="actions">
<button value="cancel">Cancel</button>
<button class="primary" value="save">Save</button>
</div>
</form>
</dialog>
</body>
</html>const dialog = document.querySelector('#rename');
document.querySelector('#open').addEventListener('click', () => {
dialog.showModal();
});* { box-sizing: border-box; }
body {
background: hsl(227 30% 17%);
color: white;
font: 1rem/1.5 system-ui, sans-serif;
margin: 0;
min-block-size: 100dvh;
padding: 2rem;
}
button, input { font: inherit; }
button {
border: 0;
border-radius: .35rem;
cursor: pointer;
padding: .7rem 1rem;
}
dialog {
background: hsl(0 0% 100%);
border: 0;
border-radius: .75rem;
box-shadow: 0 1.5rem 4rem hsl(227 40% 5% / .45);
color: hsl(227 30% 17%);
inline-size: min(90vw, 28rem);
padding: 1.5rem;
}
dialog::backdrop {
background: hsl(227 40% 5% / .7);
backdrop-filter: blur(3px);
}
form { display: grid; gap: 1rem; }
h2 { margin: 0; }
input { padding: .65rem; }
.actions { display: flex; gap: .75rem; justify-content: end; }
.primary { background: hsl(263 70% 48%); color: white; }
:focus-visible {
outline: 3px solid hsl(190 90% 45%);
outline-offset: 3px;
}[ A11y Checklist ]
- Use a modal only when interrupting the current task is necessary.
- Give the dialog an accessible name, commonly with a visible heading and
aria-labelledby. - Include a clearly labeled close or cancel button.
- Use
showModal()for modal behavior; do not addaria-modal="true"to a non-modal dialog. - Place initial focus deliberately.
autofocusis useful when the safest or most common target is clear. - For destructive actions, initial focus usually belongs on the least destructive option.
- Keep the trigger in the document so focus has a sensible place to return.
- Let Escape work unless dismissing would cause real data loss.
- Avoid putting an entire complex workflow inside a dialog.