HTML forms · 8 / 9
lesson 8

The dialog element

Modal vs non-modal, showModal, focus trap, the ::backdrop pseudo, and the form trick that closes a dialog with a value.

~ 17 min read·lesson 8 of 9
0 / 9

Modals are the place every JavaScript framework reinvents the wheel. Pages of code: focus trap, backdrop scrim, escape-key handler, body scroll lock, ARIA roles. Most of it can be replaced by <dialog>.

This lesson covers the modal use case in depth: how to open and close a dialog, how to capture the user's choice with no event handler at all, and the small details that decide whether your dialog feels like part of the platform or like a JavaScript widget.

showModal vs show

A <dialog> element is hidden by default. To show it, you call one of two JavaScript methods:

  • dialog.showModal() — opens it as a modal. Renders in the top layer (above everything), shows a backdrop, traps focus, blocks the page behind, closes on Escape.
  • dialog.show() — opens it as a non-modal. Renders in the top layer, but the page behind stays interactive. No focus trap, no backdrop, no Escape behaviour.
confirm.html
<dialog id="confirm">
<p>Delete this item? This cannot be undone.</p>
<button id="cancel">Cancel</button>
<button id="ok">Delete</button>
</dialog>

<button id="open">Delete item</button>

<script>
const dialog = document.getElementById('confirm');

document.getElementById('open').onclick = () => dialog.showModal();
document.getElementById('cancel').onclick = () => dialog.close();
document.getElementById('ok').onclick = () => {
  deleteItem();
  dialog.close();
};
</script>

showModal() opens the dialog. dialog.close() closes it. Click "Delete item" → dialog opens with focus on the first button. The user clicks Cancel or Delete, and the dialog closes.

A useful picture: a modal dialog is a phone call interrupting whatever you were doing. Until you hang up, you can't do anything else with the page. A non-modal dialog is a friend popping into your office to ask a quick question — they're there, but you can keep typing.

There is a third opening method — adding the open attribute in HTML — but it gives you a non-modal-looking element without the top-layer treatment. For real modals, always use showModal() from JavaScript.

Watch out

A dialog opened with open attribute (instead of showModal()) is not a real modal. It does not render in the top layer, does not trap focus, and does not block the page. If you find your dialog is appearing below other elements, you used the attribute when you needed the method.

The form method="dialog" trick

The most useful pattern with dialogs is the closing form. When a <form method="dialog"> lives inside a dialog, submitting the form closes the dialog. The clicked button's value becomes the dialog's returnValue.

closing-form.html
<dialog id="confirm">
<form method="dialog">
  <p>Delete this item? This cannot be undone.</p>
  <menu>
    <button value="cancel">Cancel</button>
    <button value="delete">Delete</button>
  </menu>
</form>
</dialog>

<script>
const dialog = document.getElementById('confirm');

document.getElementById('open').onclick = () => {
  dialog.showModal();
};

dialog.addEventListener('close', () => {
  if (dialog.returnValue === 'delete') {
    deleteItem();
  }
});
</script>

No click handlers on the buttons. The form takes care of closing. The user clicks "Delete" → the form submits with method="dialog" → the dialog closes with returnValue = "delete" → the dialog's close event fires → you decide what to do based on the return value.

This pattern keeps the dialog logic contained in one place. The buttons declare their intent (value="..."); the close handler reacts. No per-button event listeners.

The <menu> element is a semantic wrapper for a list of buttons. You can use a plain <div> here too — <menu> is just slightly more correct for a button row.

Styling the backdrop

A modal dialog draws a layer between the dialog and the page behind it — the backdrop. CSS targets it with the ::backdrop pseudo-element.

dialog-styling.css
dialog {
border: none;
border-radius: 12px;
padding: 1.5rem;
max-width: 32rem;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}

dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}

The backdrop covers everything behind the dialog. Style it with any colour, opacity, or even a backdrop-filter blur. The dialog itself is a regular box you can style top-to-bottom.

A common choice: animate the backdrop on open. Use the :open pseudo (or animate via the animate-allowed-discrete API) so the scrim fades in rather than appearing instantly.

animate.css
dialog[open] {
animation: pop-in 200ms ease;
}

dialog[open]::backdrop {
animation: fade-in 200ms ease;
}

@keyframes pop-in {
from { transform: scale(0.95); opacity: 0; }
to   { transform: scale(1);    opacity: 1; }
}

@keyframes fade-in {
from { opacity: 0; }
to   { opacity: 1; }
}

The [open] attribute selector targets the dialog when it is shown. Combine with keyframe animations and you get the fade-and-pop motion modal libraries used to take 50KB to provide.

Focus and escape

showModal() does the focus dance for you:

  • On open: focus moves to the first focusable element inside the dialog. If you want a specific element to receive focus instead, add autofocus to it.
  • While open: Tab cycles through focusable elements in the dialog only. Tab from the last element wraps back to the first. The page behind is inert.
  • On close: focus returns to the element that opened the dialog (the button you clicked).

That whole flow used to require a "focus trap" library. The browser ships it now.

Escape closes the dialog by default. The dialog's cancel event fires first — you can call event.preventDefault() in the cancel handler to override and keep the dialog open.

cancel-handler.js
dialog.addEventListener('cancel', (event) => {
if (formIsDirty) {
  event.preventDefault();
  confirmDiscard.showModal();
}
});

A good pattern: prevent the cancel when the user has unsaved changes, and open a "discard?" sub-dialog instead. Restoring closes the parent only if the user confirms.

Tip

Dialogs in the top layer always sit above every other element on the page. You almost never need to think about z-index with showModal(). If your dialog is hidden behind something, you forgot to call showModal().

check your understanding
You open a dialog with dialog.show() instead of dialog.showModal(). What's missing?

Non-modal dialogs

dialog.show() opens the same element non-modally. The page behind stays interactive. No backdrop. No focus trap. No Escape.

Use cases: a "tools" panel that floats in the corner, a date picker anchored to an input, an autocomplete suggestion list. The dialog draws above the page (top layer) but does not interrupt the user.

For most of those cases, the new popover API (covered in semantic-html lesson 6) is a cleaner fit — popovers have built-in light dismiss (click outside to close) and can anchor to a trigger button. <dialog> and popover overlap; pick whichever fits your needs.

The split, roughly:

  • Modal <dialog> — when the user has to make a decision before continuing.
  • Non-modal <dialog> — when you need a labelled overlay that does not auto-dismiss.
  • Popover — when you need a transient panel that disappears on outside click.
check your understanding
You build a "Confirm delete" dialog with <form method="dialog"> wrapping Cancel and Delete buttons. The user clicks Delete. The dialog closes with what returnValue?
check your understanding
Your modal dialog has a form with method="dialog". The user fills out the form and presses Enter. What happens?
check your understanding
You want to style the dim layer behind your dialog with a soft blur. Which CSS targets it?
← prevnext lesson →
KeepLearningcertificate
for completing
HTML forms
0 of 9 read