Skip to content

Keyboard Focus Trap Accessibility Fix

DodaTech Updated 2026-06-24 3 min read

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-hidden or inert on background content
  • Test every interactive component with Tab-only navigation

Common Mistakes with keyboard trap

  1. Non-exhaustive pattern matches that compile with warnings then crash at runtime
  2. Misunderstanding that String is [Char] with poor performance for large text operations
  3. Using foldl instead of foldl' 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

### What is the difference between a focus trap and a focus loss?

A focus trap keeps focus cycling within a component (user cannot leave). Focus loss means focus disappears to the body or document when a component closes (user does not know where focus is). Both are Accessibility issues. Fix traps with Tab cycle, fix loss by returning focus to the trigger.

Does the inert attribute work in all browsers?

The inert attribute is supported in Chrome 102+, Firefox 112+, and Safari 15.5+. For older browsers, use a Polyfill or manually set tabindex="-1" and aria-hidden="true" on all interactive elements.

Should autofocus be used in modals?

Yes, use autofocus on the first interactive element inside the modal. This ensures focus moves into the modal immediately. Without autofocus, the user may need to Tab multiple times before reaching the modal's content.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro