Skip to content

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

  1. Walk the page with the keyboard

    Tab through the whole page without a mouse, from the first element to the last.

  2. Mark the spot

    Where focus loops back or stops moving, that is the trap.

  3. Build an exit

    Escape closes the component, and the focus loop ends with it.

  4. 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.
Before: a loop with no exitJavaScript
// 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

After: a loop with an exit and a returnJavaScript
// 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

The native dialog elementHTML
<!-- 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

  1. Put the cursor in the address bar, then walk the page with Tab only.
  2. Note where focus stops or goes in circles.
  3. Press Escape in every overlay: does it close, and where does focus land?
  4. Check Shift-Tab as well: some traps only hold one direction.

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.