Tutorial
Finding and fixing a keyboard trap
1 min read
In short
A keyboard trap exists when focus enters an area and cannot leave it with Tab, Shift-Tab or Escape. The usual cause is a hand-built focus loop with no exit. The fix: Escape closes, the loop only runs while the component is open, and focus returns to the element that opened it. The safest option is the native dialog element.
At a glance
Walk the page with the keyboard
Tab through the whole page without a mouse, from the first element to the last.
Mark the spot
Where focus loops back or stops moving, that is the trap.
Build an exit
Escape closes the component, and the focus loop ends with it.
Return focus
After closing, put focus on the element that opened the component.
In our own severity catalog the keyboard trap is the one finding rated „critical“ that takes a whole page with it. The reason is simple: someone who cannot use a mouse cannot get past it. Everything after that point, basket and checkout included, no longer exists for them.
What usually causes it
- A hand-built focus loop in a dialog, menu or off-canvas navigation that knows no Escape.
- Embedded third-party content (video players, maps, payment widgets) that swallows Tab.
- Components that put focus nowhere on close: it ends up at the top of the page or vanishes.
// Fokusfalle: der Loop haelt den Fokus im Dialog fest,
// aber es gibt keinen Ausgang. Escape ist nicht belegt,
// und der Dialog laesst sich nur mit der Maus schliessen.
dialog.addEventListener("keydown", (event) => {
if (event.key !== "Tab") return;
const focusables = dialog.querySelectorAll("a, button, input");
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});The loop itself is correct and even required for modals. What is wrong is that there is no way out: no Escape, no way to close from the keyboard.
The fix
// Derselbe Loop, aber mit Ausgang und Rueckkehr.
let lastTrigger = null;
function openDialog(trigger) {
lastTrigger = trigger;
dialog.hidden = false;
dialog.querySelector("[autofocus], button, a, input")?.focus();
document.addEventListener("keydown", onKeydown);
}
function closeDialog() {
dialog.hidden = true;
document.removeEventListener("keydown", onKeydown);
// Fokus zurueck an den Ausloeser: sonst landet er am Seitenanfang
lastTrigger?.focus();
}
function onKeydown(event) {
if (event.key === "Escape") {
closeDialog(); // der Ausgang, den 2.1.2 verlangt
return;
}
if (event.key === "Tab") {
trapTabWithinDialog(event); // Loop nur, solange der Dialog offen ist
}
}The most robust option
<!-- Am robustesten: das native Element. Der Browser bringt
Fokus-Loop, Escape und Rueckgabe des Fokus selbst mit. -->
<dialog id="confirm">
<h2>Bestellung loeschen?</h2>
<form method="dialog">
<button value="cancel">Abbrechen</button>
<button value="confirm">Loeschen</button>
</form>
</dialog>
<script>
document.getElementById("confirm").showModal();
</script>With showModal() the browser brings the focus loop, Escape handling and focus return itself. That is far less code that can be wrong, and the pitfalls are already solved.
Testing, in two minutes
- Put the cursor in the address bar, then walk the page with Tab only.
- Note where focus stops or goes in circles.
- Press Escape in every overlay: does it close, and where does focus land?
- Check Shift-Tab as well: some traps only hold one direction.
Related WCAG criteria
Frequently asked questions
- Is a focus loop in a modal not itself a trap?
- No, as long as there is an exit. A modal is supposed to hold focus so you do not tab into the background unnoticed. It becomes a trap only when Escape and a close button are missing.
- What if a third-party widget causes the trap?
- First check whether there is an accessible configuration or a newer version. Otherwise the options are to replace the widget or to put it behind your own keyboard-operable trigger that can be left again. Hand-built workarounds inside third-party code are fragile.
- Is putting focus on the body when closing enough?
- Technically focus does leave the trap, but the operating position is lost. Put it on the triggering element so the person can continue where they were.
Request a free scan
Tell us your domain - we run a free initial scan and show you the most important barriers.