⚡IntermediateAdvanced Accessibility

Accessible Modals and Dialogs

Build accessible modal dialogs using the native <dialog> element and ARIA attributes.

Overview

The HTML5 <dialog> element is the native solution for modal and non-modal dialogs. It handles accessibility automatically: focus trapping, Escape key to close, backdrop overlay, and the appropriate ARIA semantics. For custom div-based dialogs, you must manually add: role="dialog", aria-modal="true", aria-labelledby referencing the dialog title, focus trapping, and Escape key handling. Always prefer the native <dialog> element over custom implementations it's better supported and requires far less code.

Key Points

  • •dialog.showModal() opens as modal (with backdrop, focus trap, Escape closes)
  • •dialog.show() opens as non-modal (no backdrop, no focus trap)
  • •dialog.close() closes the dialog; the close event fires
  • •<dialog> has role="dialog" and aria-modal="true" automatically when using showModal()
  • •aria-labelledby on <dialog> referencing the dialog's heading title
  • •The CSS ::backdrop pseudo-element styles the overlay behind a modal dialog

Examples

Native <dialog> as a modal

<!-- The dialog element -->
<dialog id="confirm-dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Delete</h2>
  <p>Are you sure you want to delete this item? This action cannot be undone.</p>
  <div class="dialog-actions">
    <button id="cancel-btn">Cancel</button>
    <button id="confirm-btn" class="danger">Delete</button>
  </div>
</dialog>

<!-- Trigger button -->
<button id="delete-trigger">Delete Item</button>

<script>
const dialog  = document.getElementById('confirm-dialog');
const trigger = document.getElementById('delete-trigger');

// Open modal
trigger.addEventListener('click', () => {
  dialog.showModal();
});

// Close on cancel return focus to trigger
document.getElementById('cancel-btn').addEventListener('click', () => {
  dialog.close('cancel');
});

// Close on confirm
document.getElementById('confirm-btn').addEventListener('click', () => {
  dialog.close('confirm');
  // Handle deletion...
});

// Return focus to trigger when dialog closes
dialog.addEventListener('close', () => {
  trigger.focus();
});
</script>

<style>
  /* Overlay styling */
  dialog::backdrop {
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(4px);
  }
</style>

Reading the dialog return value

dialog.addEventListener('close', () => {
  console.log(dialog.returnValue); // "cancel" or "confirm"
  if (dialog.returnValue === 'confirm') {
    deleteItem();
  }
});

dialog.close(value) sets dialog.returnValue useful for conditional post-close logic.

Custom div dialog with required ARIA (if native dialog is not feasible)

<div
  id="custom-dialog"
  role="dialog"
  aria-modal="true"
  aria-labelledby="custom-dialog-title"
  tabindex="-1"
  hidden
>
  <h2 id="custom-dialog-title">Edit Profile</h2>
  <form>
    <label for="name">Name</label>
    <input type="text" id="name" name="name">
    <button type="submit">Save</button>
    <button type="button" id="custom-close">Cancel</button>
  </form>
</div>

You must manually add focus trapping, Escape handling, and backdrop overlay.

Common Mistakes

  • Using show() instead of showModal() no focus trap or Escape key closes
  • Not adding aria-labelledby screen readers announce the dialog without context
  • Forgetting to return focus to the trigger on close keyboard users are stranded

Note

Use the native <dialog> element with showModal() it provides focus trapping and Escape key closing for free. The only customization needed is CSS for the dialog and ::backdrop styling.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What is the difference between dialog.show() and dialog.showModal()?