What is a modal dialog?

A modal dialog is a window that appears on top of the main page and demands the user's attention before they can continue. While it's open, the background content is inactive, the user must respond to the dialog before doing anything else.

Modal dialogs are everywhere: confirmation prompts, forms, alerts, cookie notices, image lightboxes. When implemented incorrectly they create significant barriers for people using keyboard navigation or screen readers, the most common failure being that keyboard focus either doesn't move into the dialog, or leaks out behind it into content the user cannot see.

The most common failure: Opening a dialog without moving keyboard focus into it. Sighted users see the dialog and click it. Keyboard users and screen reader users are left behind, their focus stays on whatever triggered the dialog, while the content they need to interact with is visually above them on screen but unreachable.

The three rules of dialog focus management

Correct focus management is mandatory under WCAG 2.2 SC 2.4.3 (Focus Order) and SC 2.1.1 (Keyboard). Every accessible dialog must follow three rules:

Move focus in

When a dialog opens, keyboard focus must move to the first focusable element inside it, or to the dialog container itself if there is no immediately actionable element.

Trap focus inside

While the dialog is open, Tab and Shift+Tab must cycle through elements within the dialog only. Focus must not leak out to the page behind it.

Return focus on close

When the dialog closes, however it closes, focus must return to the element that triggered it, so keyboard users do not lose their place on the page.

Demo 1: Native <dialog> element

The native HTML <dialog> element with showModal() is the simplest and most robust approach. The browser handles the focus trap automatically, and ::backdrop provides the overlay. No extra JavaScript focus management is needed.

Recommended for most use cases. The native <dialog> element with showModal() is supported in all modern browsers (Chrome, Firefox, Safari, Edge) and handles focus containment natively. Use it unless you have a specific reason not to.

A confirmation dialog triggered by a button. Try opening it and navigating entirely by keyboard.

Keyboard: Tab moves focus between buttons inside the dialog. Esc closes it. Focus returns to the Delete button when closed.

Delete this file?

This will permanently remove report-final-v3.pdf and cannot be undone. Are you sure you want to continue?

Demo 2: ARIA dialog with custom focus trap

For richer interactions, or when you need to support older browser environments, you can build a dialog using role="dialog" and aria-modal="true" with a JavaScript focus trap. This pattern gives you full control over behaviour and styling.

aria-modal="true" tells screen readers to treat everything behind the overlay as inert. However, not all older screen readers honour it, the focus trap JavaScript remains essential for those cases.

A form dialog built with role="dialog". Tab cycles through the form fields and buttons. Fields are validated on submit.

Keyboard: Tab / Shift+Tab cycles inside the dialog. Esc closes it without submitting.

Code examples

The following examples show the implementation for Demo 1 (native <dialog>). For most projects this is all you need.

<!-- Trigger button -->
<button
  aria-haspopup="dialog"
  id="openBtn"
  onclick="openDialog()">
  Delete file
</button>

<!-- Dialog -->
<dialog
  id="confirmDialog"
  aria-labelledby="dialogTitle"
  aria-describedby="dialogDesc">

  <h2 id="dialogTitle">Delete this file?</h2>
  <p id="dialogDesc">This action cannot be undone.</p>

  <div class="dialog-actions">
    <button
      id="cancelBtn"
      onclick="closeDialog()">Cancel</button>
    <button
      onclick="confirmDelete()">Yes, delete</button>
  </div>

</dialog>
const dialog  = document.getElementById('confirmDialog');
const openBtn  = document.getElementById('openBtn');
const cancelBtn = document.getElementById('cancelBtn');

// Open: showModal() traps focus natively
function openDialog() {
  dialog.showModal();
  cancelBtn.focus(); // move focus to first button
}

// Close: return focus to the trigger
function closeDialog() {
  dialog.close();
  openBtn.focus();
}

// Esc key, dialog handles this natively,
// but we restore focus manually on close.
dialog.addEventListener('close', () => {
  openBtn.focus();
});

// Confirm action
function confirmDelete() {
  // ... do the delete ...
  closeDialog();
}
/* Native dialog element */
dialog {
  border: none;
  border-radius: 12px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.25);
  padding: 28px;
  max-width: 440px;
  width: calc(100% - 32px);
}

/* Semi-transparent backdrop */
dialog::backdrop {
  background: rgba(15, 23, 42, 0.55);
  backdrop-filter: blur(2px);
}

/* Ensure visible focus indicators inside */
dialog :focus-visible {
  outline: 2px solid #246FC6;
  outline-offset: 2px;
  border-radius: 4px;
}

/* Button row */
.dialog-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  margin-top: 24px;
}

WCAG 2.2 success criteria

Accessible modal dialogs must meet the following Success Criteria. Failures on any of these are among the most common barriers audited by ExceedAbility in enterprise digital products.

2.1.1 Keyboard A

All functionality via keyboard

Every action in the dialog, opening, closing, cancelling, confirming, must be operable without a mouse. No keyboard traps that a user cannot escape.

2.1.2 No Keyboard Trap A

Focus containment with an exit

Focus must be contained inside the dialog while open (not leak to background content), but the user must always be able to close it, typically via Esc or a visible Close button.

2.4.3 Focus Order A

Logical, sequential focus movement

When the dialog opens, focus must move into it. When it closes, focus must return to the trigger element. Focus must not jump unexpectedly or disappear.

4.1.2 Name, Role, Value A

Semantic role and naming

The dialog container must have role="dialog" (or use <dialog>) and an accessible name via aria-labelledby pointing to the dialog's heading.

1.3.1 Info and Relationships A

Structure conveyed to assistive tech

The dialog's heading, descriptive text, form labels, and error messages must be programmatically determinable, not only conveyed through visual position or colour.

2.4.7 Focus Visible AA

Keyboard focus is always visible

Every focusable element inside the dialog must have a visible focus indicator. Do not suppress :focus-visible outlines, a user must always know where focus is.

Common failures to avoid

  • Focus does not move into the dialog on open, the most frequent failure. Always call .focus() on the first element or use showModal() which handles this.
  • Background content is still reachable by Tab, occurs when a custom overlay is used without a focus trap or inert attribute on background content.
  • ESC key does not close the dialog, native <dialog> handles this automatically. Custom dialogs need an explicit keydown listener for the Escape key.
  • Focus is not returned to the trigger on close, causes keyboard users to lose their place. Always store a reference to the trigger before opening and call .focus() on it when closing.
  • No accessible name on the dialog, without aria-labelledby pointing to the heading, screen readers announce "dialog" with no context.
  • Error messages not associated with fields, use aria-describedby to link inputs to their error message elements, and aria-invalid="true" on fields that fail validation.
Test your dialogs with a real screen reader. Open NVDA (free, Windows), JAWS, or VoiceOver (Mac/iOS) and interact with your dialogs. If the screen reader announces the dialog heading and role when it opens, and returns you to the trigger when it closes, you have the fundamentals right.

Want expert eyes on your dialogs?

ExceedAbility audits interactive components including modal dialogs, menus, and carousels, with assistive technology testing using real screen readers.

Contact ExceedAbility