Ever clicked a button to open a modal dialog and then found yourself tabbing endlessly outside it, or worse, losing keyboard focus completely? I’ve been there. You build a modal, slap on some ARIA attributes, and think it’s accessible, but your keyboard users are still wandering off into the page behind the curtain.
That’s the focus trap failing. It’s a common pitfall and surprisingly tricky to get right because browsers, assistive tech, and ARIA interplay in nuanced ways. Let me walk you through how focus management works under the hood in modal dialogs, what ARIA expects, common mistakes that break it, and practical debugging tips.
The moment I realized the focus trap was broken
I was debugging a modal that looked fine visually but had a nasty keyboard accessibility bug. When opened, keyboard focus wasn’t confined to the dialog. Tabbing would escape to elements behind the modal overlay. Screen reader users got lost. It wasn’t just a UX glitch, this was a fundamental accessibility failure.
Sure, the modal had role="dialog", aria-modal="true", and an element with tabindex="-1" to receive initial focus. But something was still off.
How browsers handle focus in modal dialogs
Here’s the deal: browsers don’t magically trap focus inside elements with aria-modal="true". This attribute signals assistive technologies that the dialog is modal and that content outside should be ignored, but it doesn’t enforce keyboard focus containment by itself.
Focus trapping relies on your JavaScript to manage the tab order:
- On modal open, move focus programmatically to a focusable element inside the dialog, usually the dialog container or first interactive element.
- Listen for
Tab and Shift+Tab key events.
- When focus reaches the last or first focusable element inside the dialog, wrap it around to the opposite end.
If you don’t do this, keyboard users can tab out of the modal, defeating its modal purpose.
What ARIA attributes actually do
role="dialog": Informs assistive tech that this element is a dialog.
aria-modal="true": Declares the dialog is modal and that the rest of the page is inert to screen readers.
aria-labelledby and aria-describedby: Link dialog label and description for screen reader context.
tabindex="-1": Makes an element programmatically focusable.
None of these automatically trap focus. They help screen readers announce the dialog and its semantics but focus management is your responsibility.
Common mistakes that break focus traps
1. Not moving focus on open
If you don’t set focus inside the dialog when it opens, keyboard users might remain focused on the triggering button or somewhere outside the modal.
modalElement.focus(); // or focus first focusable child
2. Forgetting to trap Tab and Shift+Tab
Without intercepting the tab key, focus can escape. Your handler should check if the next tab would leave the dialog and cycle focus inside.
document.addEventListener('keydown', e => {
if (e.key === 'Tab') {
// logic to keep focus inside modal
}
});
3. Including non-focusable or hidden elements in your trap
Make sure your list of focusable elements excludes disabled or aria-hidden elements. Otherwise, your logic breaks.
4. Not restoring focus on close
Good UX means returning keyboard focus to the element that opened the modal.
triggerButton.focus();
5. Overlapping modals or multiple dialogs open
Having more than one modal open can confuse focus management and screen readers. Only one modal should be active.
Debugging focus issues step-by-step
Step 1: Check your initial focus
Open your modal, then inspect document.activeElement. Is it inside the modal? If not, your initial focus is missing or incorrect.
console.log(document.activeElement);
Step 2: Identify focusable elements
Get a list of all focusable elements inside the modal. A quick way:
const focusables = modalElement.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])');
Are these correct? Are any hidden or disabled?
Step 3: Test tabbing behavior
Manually tab through the modal. Does focus wrap at the start and end? If not, your event listeners or the logic inside them need revision.
Step 4: Simulate screen reader behavior
Use browser accessibility tools (like Chrome’s Accessibility pane or VoiceOver/NVDA) to see how the dialog is announced. Is it reading the label and description?
Step 5: Look for overlapping modals
Are other dialogs or overlays present that might confuse focus or aria-modal semantics?
A concrete example of a simple, robust focus trap
Here’s a minimal pattern that worked for me:
function trapFocus(modal) {
const focusableSelectors = 'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])';
const focusableEls = Array.from(modal.querySelectorAll(focusableSelectors)).filter(el => !el.disabled && el.offsetParent !== null);
let firstEl = focusableEls[0];
let lastEl = focusableEls[focusableEls.length - 1];
modal.addEventListener('keydown', e => {
if (e.key !== 'Tab') return;
if (e.shiftKey) { // Shift + Tab
if (document.activeElement === firstEl) {
e.preventDefault();
lastEl.focus();
}
} else { // Tab
if (document.activeElement === lastEl) {
e.preventDefault();
firstEl.focus();
}
}
});
// On open
firstEl.focus();
}
This snippet:
- Collects all visible, enabled focusable elements.
- Listens for
Tab and Shift+Tab keydown events.
- Wraps focus from last to first, and first to last.
- Moves initial focus to the first focusable element.
Don’t forget to manage inertness of the rest of the page
ARIA doesn’t mandate making the rest of the page inert, but it’s good practice.
Many libraries set aria-hidden="true" on sibling content or use the inert attribute (now supported in many browsers) to prevent interaction and assistive tech focus outside the modal.
Final thoughts
Focus management in modals is deceptively tricky because ARIA helps screen readers but doesn’t enforce keyboard traps. You have to handle focus moves and tab wrapping yourself.
Remember to:
- Set initial focus inside the dialog on open
- Trap keyboard focus inside with listeners
- Restore focus on close
- Use correct ARIA roles and attributes for screen readers
- Make background content inert or hidden for assistive tech
The next time your keyboard users tell you your modal keyboard behavior is weird, now you’ll know exactly where to look under the hood.