Skip to Content
Component ExamplesModals & Dialogs

Overview

Modal dialogs are one of the trickiest components to get right. The native <dialog> element with .showModal() is now the recommended approach — it provides a built-in focus trap, automatic inert on background content, Escape key handling, and a ::backdrop pseudo-element. The old custom ARIA approach required hundreds of lines of JavaScript to achieve the same result.

WCAG Criteria:

Key requirements:

  • Use the native <dialog> element with .showModal() (preferred over custom ARIA)
  • Label the dialog with aria-labelledby pointing to its heading
  • Move focus into the dialog on open (native <dialog> does this automatically)
  • Return focus to the trigger element on close
  • Close on Escape key (native <dialog> does this automatically)

Custom ARIA Modal vs. Native Dialog

Inaccessible
<!-- 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>
Live Preview
Accessible
<!-- 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 onclick="this.closest('dialog').close()">Cancel</button> <button onclick="this.closest('dialog').close()">Delete</button> </dialog>
Live Preview

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

What the screen reader announces:

VersionAnnouncement
Custom div”Are you sure?” (just text — no dialog context)
Native <dialog>”Are you sure?, dialog” (dialog role announced, content described)

You still need to add:

  • aria-labelledby pointing to the dialog heading
  • aria-describedby for the warning/description text
  • Focus restoration on close (via the onclose event)
  • Focus the least destructive action for confirmation dialogs

Resources