Skip to Content
Component ExamplesModals & Dialogs

Modals & Dialogs

Native dialog element vs. custom ARIA modals — focus trap, aria-modal, and focus return

Maintained by Martin KrugerLast reviewed

Overview

Modal dialogs require a name, deliberate initial focus, modal background behavior, Escape handling, and contextual focus restoration. Native <dialog> with .showModal() provides the browser’s modal behavior, top-layer placement, background inertness, and a ::backdrop; authors still choose the initial focus target and where focus returns.

WCAG Criteria:

  • 2.1.2 No Keyboard Trap  requires users to be able to leave any component they can enter, or be told a non-standard exit method. Modal focus containment is an APG/native-dialog convention, not the criterion’s text.
  • 2.4.3 Focus Order  requires an order that preserves meaning and operability.
  • 4.1.2 Name, Role, Value  requires the dialog and controls to expose valid programmatic semantics.

Key requirements:

  • Use the native <dialog> element with .showModal() (preferred over custom ARIA)
  • Label the dialog with aria-labelledby pointing to its heading
  • Choose initial focus based on content and risk; use autofocus or explicit focus where necessary
  • Return focus to the trigger when it still exists, otherwise to the next logical workflow target
  • Close on Escape key (native <dialog> does this automatically)

View inaccessible code
<!-- Custom div modal — no focus trap, no Escape, no inert background --> <button onclick="document.getElementById('overlay').style.display='flex'"> Delete Account </button> <div id="overlay" style="display:none; position:fixed; inset:0;"> <div> <h2>Are you sure?</h2> <p>This action cannot be undone.</p> <button onclick="close()">Cancel</button> <button onclick="close()">Delete</button> </div> </div>
View accessible code
<!-- Native <dialog> — built-in focus trap, Escape, ::backdrop --> <button id="trigger" onclick="document.getElementById('dlg').showModal()"> Delete Account </button> <dialog id="dlg" aria-labelledby="dlg-title" aria-describedby="dlg-desc" onclose="document.getElementById('trigger').focus()" > <h2 id="dlg-title">Are you sure?</h2> <p id="dlg-desc">This action cannot be undone.</p> <button autofocus onclick="this.closest('dialog').close()">Cancel</button> <button onclick="this.closest('dialog').close()">Delete</button> </dialog>

What’s wrong with the custom div modal?

  • No dialog role — screen readers don’t announce it as a dialog
  • No focus trap — Tab key moves to elements behind the overlay
  • No Escape key handling — keyboard users can’t dismiss it
  • Background content remains interactive — users can click/tab behind the modal
  • Focus is not managed on open or close

What the native <dialog> gives you for free:

  • Automatic focus trap (Tab/Shift+Tab wraps within the dialog)
  • Escape key closes the dialog (fires the close event)
  • Background content is automatically inert (unfocusable, unclickable)
  • ::backdrop CSS pseudo-element for the overlay
  • Built-in dialog role in the accessibility tree

Expected semantics:

VersionProgrammatic result
Custom divHeading text exists, but there is no dialog role, modal state, or managed focus.
Native <dialog>A named modal dialog exposes its short description and focus begins on Cancel. Exact speech varies.

You still need to add:

  • aria-labelledby pointing to the dialog heading
  • aria-describedby when the description is short, plain text; omit it for long or structured content so users can navigate the structure
  • Focus restoration on close (via the onclose event)
  • Focus the least destructive action for confirmation dialogs

Resources