Dialogs that behave like dialogs

Handle cancel, close, and focus

Separate a request to dismiss from a completed close, then restore focus when the opener no longer exists.

Two dialog events answer different questions. cancel fires when the user requests dismissal through a platform action such as Escape. close fires after the dialog has actually closed, however that happened.

Use cancel when the request may need confirmation, and call preventDefault() only for a real reason such as unsaved changes. Use close for cleanup and state updates. Blocking Escape unconditionally removes an expected exit and can leave users trapped.

const addTask = document.querySelector("#add-task")
const taskDialog = document.querySelector("#task-dialog")
let opener
addTask.addEventListener("click", event => {
  opener = event.currentTarget
  taskDialog.showModal()
})
taskDialog.addEventListener("cancel", event => {
  if (formIsDirty()) event.preventDefault()
})
taskDialog.addEventListener("close", () => {
  if (opener?.isConnected) opener.focus()
  else document.querySelector("h1")?.focus()
})

Browsers normally restore focus to the control that opened a modal dialog. Explicit restoration becomes useful when your close handler removes or replaces that opener. Store the actual element, not only an ID that might now point to a different task card.

A heading is not normally focusable. If you use it as a recovery target, give it tabindex="-1"; this permits programmatic focus without adding it to the normal Tab order. Another stable nearby button may be an even better fallback.

Do not install your own global Escape listener to compete with the dialog. The native cancel pathway understands which top-layer element is active. If you prevent it for dirty data, show a second clear choice and make sure that confirmation itself has a working Cancel path.

Open the editor from a task-specific Edit button, delete that task while the dialog is open in a test handler, and close the dialog. Verify focus lands on a stable fallback instead of disappearing to the document body. Then type a change, press Escape, and confirm your dirty-state policy; press Escape with no changes and confirm normal dismissal.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →