Skip to main content
Up Your A11y

Modals with the Native Dialog Element

Accessible modals used to mean managing focus, inert backgrounds and escape handling by hand. The dialog element now does most of it, with a few things it still does not.

For developers who want to:

  • Understand what the native dialog element handles for you
  • Know what it still leaves to you
  • Replace a hand-rolled modal with showModal safely
  • Recognise the cases where a dialog is the wrong pattern

Building an accessible modal used to be one of the most reliable ways to get something subtly wrong. You had to move focus in, trap it, hide the rest of the page from assistive technology, handle Escape, and put focus back where it came from on close. Most implementations got three of those five right.

The native dialog element now handles most of it.

What you get for free

Opened with showModal(), a dialog gives you:

  • focus moved into the dialog when it opens
  • focus trapped inside it while open
  • the rest of the page made inert — not just visually, but for assistive technology and pointer events
  • Escape closes it
  • a backdrop you can style with the ::backdrop pseudo-element
  • the correct role and modal semantics without any ARIA

That is the list of things people used to implement by hand, and the browser now does them consistently.

Note the distinction: show() opens a non-modal dialog and does none of the trapping or inerting. showModal() is the one that gives you the behaviour above. This trips people up.

What it still leaves to you

The accessible name. A dialog with no name is announced as just "dialog". Give it one, usually by pointing aria-labelledby at its own heading.

Where focus lands. By default focus goes to the first focusable element. That is often the close button, which is a poor first thing to hear. Use autofocus on the element you actually want, which is usually the heading or the first input.

Where focus returns. Browsers generally return focus to the trigger, and if you are removing or re-rendering the trigger — common in React — you need to handle that yourself.

Scroll behaviour. The page behind can still scroll in some browsers.

Whether it should be a dialog at all, which is the part worth thinking about hardest.

A minimal correct version

The shape, without framework specifics:

  • a dialog element containing a heading and the content
  • aria-labelledby on the dialog pointing at the heading's id
  • autofocus on the sensible first target
  • a close button with a real accessible name, not just an icon
  • open with showModal(), close with close()
  • listen for the close event to run anything that needs to happen afterwards

That is markedly less code than the equivalent hand-rolled component, and markedly harder to get wrong.

In React

Two things to be careful about.

The element is imperative. You open and close it by calling methods on the node, not by rendering it conditionally. Keep a ref, and drive it from an effect that responds to your state. Rendering {isOpen && <dialog>} and expecting modal behaviour does not work — the dialog must exist and have showModal() called on it.

Returning focus. If closing the dialog also unmounts or re-renders the trigger, the browser has nowhere to return focus to and it falls back to the body. That is disorienting for a keyboard user, and it is the same class of problem as the focus-on-route-change issue covered elsewhere on this site. Capture the trigger and restore focus explicitly if you cannot guarantee it survives.

When a dialog is the wrong answer

The pattern is overused, and the accessibility question is often "should this interrupt at all".

A form with more than a couple of fields usually belongs on a page, where the browser's own navigation, back button and zoom behaviour work normally.

Content someone might want to refer back to should not be behind something that has to be dismissed.

Anything triggered without user intent — arriving on a page and being interrupted — is a usability problem before it is an accessibility one, and it is worse for someone using magnification, who may not see the trigger at all.

A cookie banner or a notification is not a modal, and making it one traps people in it.

Checking it

Four things, by hand:

Open it with the keyboard. Does focus land somewhere sensible and is it announced?

Tab all the way round. Does focus stay inside?

Press Escape. Does it close?

Check where focus went afterwards. Back to the trigger, or lost to the body?

If all four are right, the hard parts are done.

[Suzanne: you may want to add a demo page for this in the same style as the layouts and focus demos — a hand-rolled modal alongside a native one, so people can tab through both. I have not built it, since the existing demos are yours and I did not want to guess at the house style.]