auditmysitev2.0.2

Keyboard trap — focus cannot leave the element

keyboard-trap

WCAG success criterion
2.1.2 (level A)
Outcome class
Manual. A tool cannot decide this check. The audit only points a person at it; it is never counted as automatically verified.
Area
Accessibility, Navigation & Operation
Default severity
critical
axe-core rule
keyboard-trap

Why we flag this

The page becomes effectively unusable for keyboard users.

Missing focus management in modal elements.

What the rule checks

Scope. Pages where keyboard focus can be moved to a component: at least one focusable node other than the document root.

Expectation. On every page with a focusable component: one page-wide UNTESTED note (needs_review). In addition one needs_review per modal dialog in the accessibility tree (role dialog/alertdialog with modal=true: aria-modal="true" or a native dialog opened with showModal()), because a modal deliberately keeps focus and must let it go again. A non-modal dialog (no aria-modal, or dialog.show()) adds nothing. A page without any focusable component is not applicable: no note. Never pass or violation.

Why a person has to decide. Whether focus can leave a component shows only by operating it with the keyboard (Tab, Shift+Tab, Escape); the accessibility tree cannot tell a trap from a well-behaved modal.

Source. WCAG 2.2 SC 2.1.2 No Keyboard Trap; WAI-ARIA 1.2 aria-modal, HTML §4.11.4 dialog showModal()

Detection: tree; needs ax_tree.

Failing example

The check flags this for review. Corpus case interaction_dialog_native. The page may contain further findings for other rules.

interaction_dialog_native.htmlHTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Cookie consent dialog</title>
    <style>
        body { font-family: sans-serif; margin: 0; padding: 16px; color: #111; background: #fff; }
        a, button { display: inline-block; min-width: 44px; min-height: 44px; padding: 12px; box-sizing: border-box; margin: 4px; }
        .skip { position: absolute; left: -9999px; }
        .skip:focus { left: 16px; }
        .panel { border: 1px solid #333; padding: 16px; margin: 16px 0; }
    </style>
</head>
<body>
    <a class="skip" href="#main">Skip to main content</a>
    <header><p>Example Garden</p></header>
    <nav aria-label="Main">
        <a href="/" aria-current="page">Home</a>
        <a href="/plants">Plants</a>
    </nav>
    <main id="main">
        <h1>Summer pruning</h1>
        <p>Cut back lavender after flowering.</p>

        <!-- needs_review (2.1.2): a native dialog that a script opens with showModal() after load -->
        <dialog id="dlg-native-modal" aria-labelledby="dlg-native-modal-title">
            <h2 id="dlg-native-modal-title">Cookie settings</h2>
            <p>We only use cookies that the site needs.</p>
            <button type="button">Accept</button>
            <button type="button">Settings</button>
        </dialog>
    </main>
    <!-- Outside every landmark, but inert behind the open modal: nobody reaches it, so it is
         not content-outside (#783). -->
    <div class="promo"><p>Seeds half price this week.</p></div>
    <footer><p>Example Garden footer</p></footer>

    <script>
        document.getElementById('dlg-native-modal').showModal();
    </script>
</body>
</html>

This page is generated from the rule catalogue and the rule contracts; see therule overview for how the outcome classes are derived.