skip to content

What does HTML's <dialog> element give you that a hand-built div-and-overlay modal does not, and what happens when a form inside it uses method="dialog"?

level: middleimportance: should knowfreq 46%

answer

  1. hidden until opened, two ways to open
  2. top layer beats every z-index
  3. the rest of the page goes inert
  4. submitting closes instead of sending
  5. the pressed button becomes returnValue

basics

~20 s

Opened as a modal, a dialog renders in the top layer, makes the rest of the page inert, moves focus inside, closes on Escape and returns focus afterwards. A form with method="dialog" closes it instead of submitting, setting returnValue from the pressed button.

solid answer

~40 s

A `<dialog>` is hidden until opened, and opening it as a modal gives you behaviour a `<div>` cannot fake cheaply: it moves to the browser's top layer so nothing behind can paint over it, the rest of the document becomes inert — unclickable, untabbable and hidden from assistive technology — focus moves into the dialog, Escape closes it, and focus returns to the element that opened it. Inside, a `<form method="dialog">` is the markup-only close button: submitting it sends nothing anywhere, closes the dialog, and sets `dialog.returnValue` to the submitting button's `value`, so you can tell Confirm from Cancel. A button can also opt in individually with `formmethod="dialog"`. Two caveats: `method="dialog"` only works for a form inside a dialog, and the modal does not stop the page behind it from scrolling.

code

html · 15 lines
html
<button id="open">Delete project</button>

<dialog id="confirm" aria-labelledby="confirm-title">
  <form method="dialog">
    <h2 id="confirm-title">Delete this project?</h2>
    <button value="cancel" formnovalidate>Cancel</button>
    <button value="delete" autofocus>Delete</button>
  </form>
</dialog>

<script>
  const dialog = document.getElementById('confirm');
  document.getElementById('open').addEventListener('click', () => dialog.showModal());
  dialog.addEventListener('close', () => console.log(dialog.returnValue));
</script>

go deeper

for a junior

Know that a dialog element is hidden until opened, that opening it as a modal is what gives the backdrop and Escape behaviour, and that a form with method="dialog" closes the dialog instead of submitting anything.

for a middle

Explain the modal package in mechanics: top layer instead of z-index, the rest of the document inert, focus entry via autofocus, Escape firing cancel then close, focus restored on close, and returnValue taken from the submitting button.

for a senior

Show where the native element stops — background scrolling, accessible naming, dismiss buttons blocked by validation — and describe what you add deliberately. Be able to argue why a hand-built modal reproduces the look but not the behaviour.

for a principal

Own the decision for a codebase: adopt the native element as the single modal primitive versus keeping a library, what that migration costs, and how you prevent teams from re-implementing overlay layering and focus handling in every feature.

## The element `<dialog>` is hidden by default. It becomes visible when it is opened, and there are two ways to open it that are not equivalent. ```html <dialog id="confirm"> <form method="dialog"> <h2>Delete this project?</h2> <p>This cannot be undone.</p> <button value="cancel">Cancel</button> <button value="delete" autofocus>Delete</button> </form> </dialog> ``` Calling `showModal()` opens it **modally**. Calling `show()` — or writing the `open` attribute by hand — opens it **non-modally**, which is a very different thing: no backdrop, no inertness, no Escape handling, no top layer. Almost everyone who says "the dialog element didn't work" set the `open` attribute and got the non-modal behaviour. ## What modal mode actually provides 1. **Top layer.** The dialog is painted above everything else in the document, outside the normal stacking order. This ends the z-index arms race in which a modal loses to a sticky header or a transformed ancestor creating a stacking context. 2. **The rest of the document becomes inert.** Content outside the dialog cannot be clicked, cannot be reached with Tab, and is hidden from assistive technology. That last part is what most hand-built modals miss: a screen-reader user can otherwise arrow straight out of the modal into the page behind it, which is still "there" as far as the accessibility tree is concerned. 3. **Focus moves in.** Mark the element that should receive it with `autofocus`; otherwise the browser chooses, and the choice is not something to depend on. This matters for keyboard users, who would otherwise be left with focus on a button behind an inert page. 4. **Escape closes it**, firing a `cancel` event first so you can intervene, then `close`. 5. **Focus is restored** to the element that had it when the dialog opened, so a keyboard user is put back where they were rather than at the top of the document. ## What it does not provide The background still scrolls. `<dialog>` makes the page inert to interaction but does not lock scrolling, so a wheel or a touch drag over the backdrop still moves the page behind. Suppressing that is a styling concern you handle yourself. It also does not write your labelling. Give the dialog an accessible name — typically by pointing at its heading — so it is announced as something rather than as an anonymous dialog. ## method="dialog" A form is normally a request: serialise the fields, go somewhere. `method="dialog"` says the form's purpose is this dialog, and submitting it performs a *close* instead: - No network request, no navigation, no page reload. - The dialog closes and fires its `close` event. - `dialog.returnValue` is set to the `value` of the button that submitted, or the empty string if there was no submitter or it had no value. That is how a single form distinguishes Confirm from Cancel without any click handlers on the buttons. In the snippet above, pressing Delete closes the dialog with `returnValue === "delete"`; pressing Cancel closes it with `"cancel"`; pressing Escape closes it and leaves `returnValue` unchanged — which is a meaningful third case your code should handle, because "dismissed" is not the same as "cancelled by button". Two behaviours worth knowing. Interactive validation still runs: a `required` field inside a `method="dialog"` form will block the close and show the browser's message, unless you set `novalidate` or use `formnovalidate` on the dismiss button — which you usually want on Cancel, so that abandoning a half-filled form is not blocked by its own validation. And `method="dialog"` is inert outside a dialog: put such a form on the page and submitting it does nothing at all. If only one button should close and another should really submit, keep the form's normal `method` and put `formmethod="dialog"` on the dismiss button. ## The judgment part The reason interviewers ask this is not trivia about an element. It is the native-first argument in miniature: a modal is a pile of subtle behaviours — layering, inertness, focus entry, focus return, Escape, dismissal semantics — and a `<div>` implementation reproduces the *look* on day one and the *behaviours* never. Reaching for `<dialog>` and knowing precisely which of those behaviours it does and does not hand you, so you can consciously fill the rest, is the answer being probed.

  • What is the difference between opening a dialog with showModal() and setting its open attribute?
    They produce different modes. `showModal()` opens it modally: top layer, backdrop, the rest of the document inert, Escape closes it, and focus moves in and is restored afterwards. Setting the `open` attribute — like calling `show()` — opens it non-modally: it renders in normal flow with no backdrop, the page behind stays fully interactive, and Escape does nothing. Most "dialog doesn't behave like a modal" reports are this mistake.
  • How do you distinguish a dialog dismissed with Escape from one closed by the Cancel button?
    By the `returnValue`. A button submitting a `method="dialog"` form sets `returnValue` to that button's `value`, so Cancel yields `"cancel"`. Escape closes without a submitter, so `returnValue` is left as it was — typically the empty string — and a `cancel` event fires first. Treat the empty case as "dismissed" and reset `returnValue` before each open so a stale value from a previous run cannot be misread.
  • Does the native modal dialog stop the page behind it from scrolling?
    No. It makes the background inert to clicks, tab order and assistive technology, but scrolling is not blocked — a wheel or touch drag over the backdrop still moves the page underneath. You have to suppress that yourself while the dialog is open. It is worth naming explicitly, because people assume inertness covers scrolling and then ship a modal whose background drifts.
  • A form inside a dialog has a required field, and users complain the Cancel button does nothing. What is happening?
    Interactive validation runs on submission regardless of `method="dialog"`, so pressing Cancel triggers the browser's constraint check, the empty required field blocks it, and the dialog stays open with a validation bubble. Put `formnovalidate` on the Cancel button — or `novalidate` on the form if nothing there should ever be validated — so dismissing is never blocked by a form the user is abandoning.

saying these in an interview costs you the question

  • Setting the open attribute and expecting modal behaviour
  • Assuming a method="dialog" form sends data to the server
  • Thinking the modal locks background scrolling
  • Believing z-index still governs a modal dialog's stacking
  • Expecting focus to land somewhere sensible without autofocus

context