Keyboard Focus Trap Accessibility Fix
In this tutorial, you'll learn about Keyboard Focus Trap Accessibility Fix. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
The Problem
A keyboard focus trap occurs when a user cannot move focus away from a UI element using the keyboard (Tab, Esc). This makes the page unusable for keyboard-only users. Modals, custom menus, and overlay components commonly have this issue.
Quick Fix
Step 1: Allow Escape key to close modals
// Wrong — modal traps focus with no escape
function openModal() {
const modal = document.getElementById('modal');
modal.style.display = 'block';
document.getElementById('first-input').focus();
}
// Right — handle Escape key
function openModal() {
const modal = document.getElementById('modal');
modal.style.display = 'block';
document.getElementById('first-input').focus();
modal.addEventListener('keydown', handleModalKeydown);
}
function handleModalKeydown(event) {
if (event.key === 'Escape') {
closeModal();
}
if (event.key === 'Tab') {
trapFocus(event, modal);
}
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
document.getElementById('open-button').focus(); // Return focus
}
Step 2: Implement focus trapping
function trapFocus(event, container) {
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
Step 3: Manage focus when opening and closing
// Wrong — focus is not managed
function openMenu() {
const menu = document.getElementById('menu');
menu.style.display = 'block';
}
// Right — move focus into the menu
function openMenu() {
const menu = document.getElementById('menu');
menu.style.display = 'block';
menu.querySelector('a').focus(); // Focus first item
}
// When closing, return focus to the trigger element
function closeMenu() {
const menu = document.getElementById('menu');
menu.style.display = 'none';
document.getElementById('menu-trigger').focus();
}
Step 4: Use aria-hidden to hide inactive content
<!-- Wrong — background content still reachable via Tab -->
<div class="modal" role="dialog" aria-modal="true">
<button>Close</button>
<p>Modal content</p>
</div>
<div class="background-content">
<a href="/page">This link is still tabbable!</a>
</div>
<!-- Right — hide background content -->
<div class="modal" role="dialog" aria-modal="true">
<button>Close</button>
<p>Modal content</p>
</div>
<div class="background-content" aria-hidden="true" inert>
<a href="/page" tabindex="-1">Not tabbable while modal is open</a>
</div>
Step 5: Use the inert attribute
<!-- Modern approach: use inert (Chrome 102+, Firefox 112+) -->
<div class="modal" role="dialog" aria-modal="true">
<button id="modal-close">Close</button>
<p>Modal content</p>
</div>
<!-- inert makes the entire subtree non-interactive -->
<div id="page-content" inert>
<p>This content is inert while the modal is open.</p>
<a href="/page">This link is not focusable</a>
</div>
Prevention
- Always handle the Escape key in modals and overlays
- Trap Tab focus within open modals (cycle between first/last element)
- Move focus into the modal when it opens, back to trigger when it closes
- Use
aria-hiddenorinerton background content - Test every interactive component with Tab-only navigation
Common Mistakes with keyboard trap
- Non-exhaustive pattern matches that compile with warnings then crash at runtime
- Misunderstanding that
Stringis[Char]with poor performance for large text operations - Using
foldlinstead offoldl'causing stack overflow on large lists
These mistakes appear frequently in real-world A11Y code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.
Practice Exercise
Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.
This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro