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:
- On open: Move focus to the first focusable element inside the modal, or to the modal container itself if it has a
tabindex="-1". - On close: Return focus to the element that triggered the modal. Store a reference to
document.activeElementbefore 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.