The word "trap" cuts both ways when you build a modal, and a good one gets both meanings right. While a dialog is open, keyboard focus should be trapped inside it — pressing Tab must not wander off into the page behind. But a keyboard or screen-reader user must never be trapped by the modal in the other sense: unable to leave, unable to find their place again, stranded. Most broken modals fail one of these while attempting the other.

The native <dialog> element gets you most of the way, but "most of the way" leaves real gaps. Here is what a modal actually has to do, in the order it has to do it.

Open it as a true modal

There are two ways to show a <dialog>, and only one creates a modal. The show() method displays it non-modally, leaving the rest of the page interactive. The one you almost always want is showModal().

const dialog = document.querySelector('dialog');
openButton.addEventListener('click', () => dialog.showModal());

Calling showModal() does three things you would otherwise wire up by hand: it makes everything outside the dialog inert, so background content can't be clicked or tabbed to; it renders the dialog in the top layer, above everything else regardless of stacking context; and it enables the Escape key to close the dialog automatically. That last one is a genuine requirement, not a nicety — Escape must dismiss a modal, and here you get it for free.

Move focus in — to the right place

When the dialog opens, focus must move inside it, or a keyboard user is still stranded on the page behind. The browser will move focus to the first focusable element by default, but "first focusable" is often the close button in the corner, which is a poor landing spot. Take control with the autofocus attribute on the element that should receive focus — usually the first meaningful field or the primary heading made focusable.

<dialog>
  <h2>Rename project</h2>
  <input type="text" autofocus>
  <button value="save">Save</button>
</dialog>

Give it an accessible name

A screen reader announces the dialog when it opens, and it needs something to announce. Point the dialog at its own heading with aria-labelledby so the announcement is the dialog's actual title rather than silence.

<dialog aria-labelledby="dialog-title">
  <h2 id="dialog-title">Rename project</h2>
</dialog>

Return focus when it closes — the step everyone forgets

This is the single most-skipped requirement, and its absence is what makes a modal feel like a trap. When the dialog closes, focus must go back to the element that opened it. Without this, focus silently resets to the top of the document, and a keyboard user has to tab through the entire page again to get back to where they were. It is disorienting in a way sighted mouse users never notice, which is exactly why it gets missed.

The native element does not guarantee this return in every case, so do it yourself. Remember what had focus before you opened, and restore it after close.

let opener;
openButton.addEventListener('click', () => {
  opener = document.activeElement;
  dialog.showModal();
});
dialog.addEventListener('close', () => {
  opener?.focus();
});

Let a backdrop click close it — carefully

Clicking the dimmed backdrop is a common way to dismiss a modal, but the dialog's backdrop is not a separate element you can listen on — it's the ::backdrop pseudo-element. The reliable technique is to listen for clicks on the dialog itself and check whether the click landed outside the dialog's content box.

dialog.addEventListener('click', (e) => {
  const box = dialog.getBoundingClientRect();
  const inside =
    e.clientX >= box.left && e.clientX <= box.right &&
    e.clientY >= box.top && e.clientY <= box.bottom;
  if (!inside) dialog.close();
});

The whole thing, as a list

Run through this and a modal will be sound for keyboard and screen-reader users alike: open it with showModal() so the background goes inert and Escape works; place initial focus deliberately with autofocus; name it with aria-labelledby pointing at its heading; return focus to the opener on close; and allow a backdrop click to dismiss it. The through-line is that a modal has to hold focus while it's open and hand it back gracefully when it's done. Get that pair right and nobody — mouse, keyboard, or screen reader — ends up trapped.