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.
<!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.