Why Accessible Modals Matter

Modal dialogs are one of the most accessibility-problematic components on the web. When implemented poorly, they trap keyboard users outside the dialog, leave screen reader users confused about context, and fail entirely for people using assistive technologies. The good news: fixing these issues is well-understood and not technically complex.

The Essential ARIA Attributes

Every modal dialog must carry the right ARIA attributes to communicate its nature to assistive technologies:

Attribute Value Purpose
role dialog or alertdialog Identifies the element as a dialog to screen readers
aria-modal true Tells screen readers to ignore content outside the dialog
aria-labelledby ID of the title element Announces the dialog's name when it opens
aria-describedby ID of description element Optionally provides additional context

Use role="alertdialog" specifically when the modal requires an immediate response from the user, such as a confirmation prompt before a destructive action.

Focus Trapping: The Core Challenge

When a modal opens, keyboard focus must be contained within it. Without focus trapping, pressing Tab repeatedly will eventually move focus back to the page behind the modal — rendering the interface unusable for keyboard-only users.

Implementing a Focus Trap

The approach is to find all focusable elements inside the modal and intercept Tab and Shift+Tab to cycle only through those elements:

function trapFocus(modalElement) {
  const focusable = modalElement.querySelectorAll(
    'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  modalElement.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  });
}

Focus Management on Open and Close

Proper focus management is a two-part requirement:

  1. On open: Move focus to the first focusable element inside the modal, or to the modal container itself if it has a tabindex="-1".
  2. On close: Return focus to the element that triggered the modal. Store a reference to document.activeElement before opening.
let previousFocus;

function openModal(modal) {
  previousFocus = document.activeElement;
  modal.removeAttribute('hidden');
  modal.querySelector('button, [tabindex]').focus();
}

function closeModal(modal) {
  modal.setAttribute('hidden', '');
  if (previousFocus) previousFocus.focus();
}

Making Background Content Inert

The inert attribute is a modern, elegant solution to prevent interaction with background content. When applied to everything outside the modal, it disables all pointer events, keyboard interaction, and hides elements from the accessibility tree:

// When modal opens
document.getElementById('main-content').setAttribute('inert', '');

// When modal closes
document.getElementById('main-content').removeAttribute('inert');

Browser support for inert is now excellent across modern browsers, making it the preferred approach over manual tabindex manipulation.

Testing Your Modal's Accessibility

  • Keyboard test: Open the modal with Enter/Space, tab through all interactive elements, and close with Escape.
  • Screen reader test: Use NVDA (Windows), JAWS (Windows), or VoiceOver (macOS/iOS) to verify announcements.
  • Automated scan: Run tools like axe-core or Lighthouse to catch obvious violations.

Accessible modals aren't an afterthought — they're a baseline requirement. These patterns protect all users and ensure your interface works across every input method.