Keyboard Traps erkennen und vermeiden
A11Y
WCAG
Barrierefreiheit · Tastaturnavigation · WCAG 2.1.2 · Frontend
Keyboard Traps erkennen und vermeiden
Fokus-Fallen finden, testen und sauber beheben

Ein Keyboard Trap entsteht, wenn der Tastaturfokus in ein Modal, Dropdown oder Drittanbieter-Widget hineinwandert und mit Tab oder Umschalt Tab nicht wieder herausfindet. Für Tastaturnutzer und Screenreader-Nutzer bedeutet das einen kompletten Blockadepunkt. Dieser Artikel zeigt typische Ursachen, ein systematisches Testverfahren und eine korrekte Fokus-Trap-Implementierung, die mit der Escape-Taste zuverlässig wieder verlassen werden kann.

13 Min. Lesezeit WCAG 2.1.2 · Fokus-Management · Focus Trap Modal Dialogs · Custom Widgets · axe-core

1. Was ist ein Keyboard Trap?

Ein Keyboard Trap liegt vor, wenn der Tastaturfokus in eine Komponente hineinwandert, sie aber mit den üblichen Tastaturmitteln, also Tab, Umschalt Tab, Pfeiltasten oder Escape, nicht mehr verlassen kann. Das Erfolgskriterium WCAG 2.1.2 „Kein Keyboard Trap“ ist Teil der Konformitätsstufe A und damit eine der grundlegendsten Anforderungen der gesamten WCAG überhaupt. Ohne diese Eigenschaft ist eine Seite für Tastaturnutzer im schlimmsten Fall gar nicht mehr bedienbar, egal wie gut alle anderen Kriterien erfüllt sind.

In der Praxis wirkt ein Keyboard Trap wie ein eingefrorener Browser, obwohl die Seite technisch einwandfrei läuft. Betroffen sind vor allem Nutzer, die ausschließlich mit der Tastatur navigieren, etwa wegen motorischer Einschränkungen, und Screenreader-Nutzer, deren gesamte Interaktion über Tastaturbefehle läuft. Der einzige Ausweg ist häufig ein komplettes Neuladen der Seite, wodurch bereits eingegebene Formulardaten verloren gehen. Seit Inkrafttreten des Barrierefreiheitsstärkungsgesetzes ist das für viele Onlineshops kein theoretisches Risiko mehr, sondern ein konkreter rechtlicher Prüfpunkt.

2. Ursache 1: Defekte Modal-Implementierungen

Modals sind die häufigste Quelle für Keyboard Traps, weil Entwickler den Fokus absichtlich in ihnen halten wollen, dabei aber die Ausstiegslogik vergessen. Ein typisches Muster: Ein keydown-Listener fängt jeden Tab-Druck ab und schickt den Fokus zurück auf ein festes Element, unabhängig davon, wo sich der Fokus gerade befindet. Ohne einen funktionierenden Escape-Handler und ohne echte Zyklus-Logik zwischen erstem und letztem fokussierbaren Element bleibt der Nutzer damit dauerhaft in genau diesem einen Feld gefangen.

Ein zweites verbreitetes Problem ist ein Modal, das den Fokus beim Öffnen gar nicht gezielt setzt, sodass der Browser weiter im Hintergrundinhalt tabbt, während der sichtbare Dialog optisch im Vordergrund liegt. Das ist zwar kein klassischer Trap im engeren Sinn, erzeugt aber dieselbe Verwirrung, weil sichtbarer Fokus und tatsächlicher Fokus auseinanderfallen. Beide Fehlerbilder entstehen fast immer, weil die Fokuslogik nachträglich in bestehenden Code eingebaut wurde, statt von Anfang an mitgedacht zu werden.


<!-- BROKEN: modal markup with no role, no aria-modal, no exit logic -->
<div id="promo-modal" class="modal-overlay" style="display: none;">
  <div class="modal-content">
    <p class="modal-title">Sonderangebot sichern</p>
    <input type="email" placeholder="E-Mail-Adresse">
    <button id="modal-submit">Anmelden</button>
    <button id="modal-close">Schließen</button>
  </div>
</div>

<script>
// Every Tab press is redirected back to the first input field.
// There is no cycle logic and no Escape handler at all,
// so the "Schließen" button can never be reached via keyboard.
document.getElementById('promo-modal').addEventListener('keydown', (event) => {
  if (event.key !== 'Tab') return;
  event.preventDefault();
  document.getElementById('modal-submit').focus();
});
</script>

3. Ursache 2: Third-Party-Widgets und iframes

Eingebettete Drittanbieter-Komponenten wie Zahlungs-Widgets, Kartenansichten, Video-Player oder Chat-Bots sind ein besonders tückischer Auslöser für Keyboard Traps, weil der eigene Code auf den internen Zustand des Widgets keinen Zugriff hat. Läuft die Komponente in einem iframe, endet die eigene Fokuskontrolle an der Dokumentgrenze. Verhält sich das Skript im iframe fehlerhaft und fängt Tab-Ereignisse ab, ohne sie wieder freizugeben, lässt sich das von außen weder verhindern noch reparieren, ohne den Anbieter zu kontaktieren.

Ein weiteres Muster betrifft eingebettete Widgets ohne iframe, etwa React- oder Vue-Komponenten von Drittanbietern, die direkt ins DOM gerendert werden. Solche Komponenten bringen häufig eigene Tastatur-Handler mit, die mit dem umgebenden Fokus-Trap des Modals kollidieren, in das sie eingebettet sind. Zwei konkurrierende Trap-Implementierungen in verschachtelten Komponenten sind ein klassisches Rezept für einen Keyboard Trap, den keine der beiden Seiten allein verursacht, sondern erst das Zusammenspiel beider.

4. Ursache 3: Custom Dropdowns und Comboboxen

Selbstgebaute Dropdowns und Comboboxen, die die native <select>-Semantik durch div- und li-Strukturen mit ARIA-Rollen nachbilden, sind ein häufiger Trap-Kandidat, weil Entwickler Tab und Pfeiltasten miteinander verwechseln. Die WAI-ARIA Authoring Practices sehen vor, dass Pfeiltasten innerhalb einer offenen Listbox oder Combobox navigieren, während Tab das gesamte Widget verlässt und zum nächsten fokussierbaren Element springt. Wird Tab wie eine weitere Pfeiltaste behandelt, kann der Nutzer die Komponente über die Tastatur nie wieder verlassen.

Besonders kritisch wird es bei mehrstufigen Dropdowns mit Unterkategorien, wie sie in Magento-Kategorienavigationen häufig vorkommen. Jede Verschachtelungsebene bringt einen eigenen keydown-Handler mit, und wenn nur eine Ebene Tab nicht korrekt durchreicht, ist der gesamte Baum betroffen. Das folgende Beispiel zeigt den typischen Fehler: Ein Handler, der Tab wie ArrowDown behandelt und dabei jede Möglichkeit zum Verlassen der Liste unterbindet.


// BROKEN: this keydown handler traps focus inside the dropdown
// because Tab is intercepted like an arrow key
dropdownButton.addEventListener('keydown', (event) => {
  const options = dropdownList.querySelectorAll('[role="option"]');
  const currentIndex = [...options].findIndex((o) => o === document.activeElement);

  switch (event.key) {
    case 'ArrowDown':
    case 'Tab': // Bug: Tab should move focus OUT, not cycle inside
      event.preventDefault();
      options[(currentIndex + 1) % options.length].focus();
      break;
    case 'ArrowUp':
      event.preventDefault();
      options[(currentIndex - 1 + options.length) % options.length].focus();
      break;
    // No 'Escape' case at all: users cannot close and leave the widget
  }
});

5. Systematisches Testen auf Keyboard Traps

Der zuverlässigste Test ist auch der einfachste: Maus und Trackpad komplett weglegen und jede interaktive Seite ausschließlich mit Tab, Umschalt Tab, Enter, Leertaste, Pfeiltasten und Escape bedienen. Jedes Modal, jedes Dropdown und jedes eingebettete Widget wird gezielt geöffnet, anschließend wird mehrfach hintereinander Tab gedrückt, um zu prüfen, ob der Fokus irgendwann wieder außerhalb der Komponente landet oder für immer im selben Element kreist. Parallel dazu wird bei jeder geöffneten Komponente separat getestet, ob Escape sie zuverlässig schließt.

Ein strukturiertes Testprotokoll deckt typischerweise drei Fälle pro Komponente ab: Öffnen und Tab bis zum letzten fokussierbaren Element, Öffnen und sofortiges Escape, sowie Öffnen mit anschließendem Umschalt Tab vom ersten Element aus. Bricht einer dieser drei Fälle das erwartete Verhalten, liegt ein Trap oder zumindest eine fehlerhafte Fokusgrenze vor. Wichtig ist außerdem, denselben Test in mindestens zwei Browsern durchzuführen, da sich die Standard-Tab-Reihenfolge bei bestimmten Formularelementen zwischen Chromium und Firefox unterscheiden kann.

6. Eine korrekte Fokus-Trap-Implementierung bauen

Eine korrekte Fokus-Trap-Implementierung braucht drei Bausteine: eine Liste aller aktuell sichtbaren fokussierbaren Elemente innerhalb des Containers, eine Zyklus-Logik, die beim letzten Element mit Tab wieder zum ersten springt und umgekehrt bei Umschalt Tab vom ersten zum letzten, sowie einen globalen Escape-Handler, der unabhängig vom aktuellen Fokuselement immer funktioniert. Die Liste der fokussierbaren Elemente darf dabei nicht einmalig beim Öffnen berechnet werden, sondern muss bei jedem Tab-Druck neu ermittelt werden, weil sich der Inhalt eines Modals durch Nutzerinteraktion jederzeit ändern kann.

Wichtig ist außerdem, versteckte oder unsichtbare Elemente aus dieser Liste auszuschließen, etwa Elemente mit display: none oder solche, die per hidden-Attribut ausgeblendet sind. Die folgende Klasse implementiert genau dieses Muster als wiederverwendbare Komponente, die für jedes Modal instanziiert werden kann, unabhängig vom restlichen Framework.


// FocusTrap: keeps keyboard focus inside a container,
// but Escape always exits, no matter where focus currently is
class FocusTrap {
  constructor(container, { onEscape } = {}) {
    this.container = container;
    this.onEscape = onEscape;
    this.previouslyFocused = document.activeElement;
    this.handleKeydown = this.handleKeydown.bind(this);
  }

  getFocusableElements() {
    const selector = [
      'a[href]', 'button:not([disabled])', 'input:not([disabled])',
      'select:not([disabled])', 'textarea:not([disabled])',
      '[tabindex]:not([tabindex="-1"])'
    ].join(',');
    return Array.from(this.container.querySelectorAll(selector))
      .filter((el) => el.offsetParent !== null);
  }

  activate() {
    document.addEventListener('keydown', this.handleKeydown, true);
    const focusable = this.getFocusableElements();
    (focusable[0] || this.container).focus();
  }

  deactivate() {
    document.removeEventListener('keydown', this.handleKeydown, true);
  }

  handleKeydown(event) {
    // Escape always wins, regardless of the currently focused element
    if (event.key === 'Escape') {
      event.preventDefault();
      this.deactivate();
      this.onEscape?.(this.previouslyFocused);
      return;
    }

    if (event.key !== 'Tab') return;

    const focusable = this.getFocusableElements();
    if (focusable.length === 0) {
      event.preventDefault();
      return;
    }

    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  }
}

7. Escape-Taste und Fokus-Rückgabe richtig umsetzen

Escape muss in jedem Zustand des Dialogs funktionieren, auch dann, wenn der Fokus gerade in einem Eingabefeld sitzt, in dem Escape normalerweise andere Funktionen hat, etwa das Verlassen eines Suchfelds. Der Listener sollte deshalb mit capture: true registriert werden, damit er ausgelöst wird, bevor tiefer verschachtelte Komponenten das Ereignis abfangen und stoppen können. Genauso wichtig wie das Schließen selbst ist die Frage, wohin der Fokus danach wandert: Fällt er zurück auf <body>, verliert der Nutzer jeden Kontext und muss sich die Seite komplett neu erschließen.

Die korrekte Lösung merkt sich vor dem Öffnen des Dialogs das Element, das gerade den Fokus hatte, meist der Button, der das Modal ausgelöst hat, und setzt den Fokus beim Schließen exakt dorthin zurück. Visuell sollte dieser Rücksprung genauso deutlich sichtbar sein wie der Einsprung beim Öffnen, damit Sehende und Screenreader-Nutzer gleichermaßen erkennen, wo sie sich gerade befinden. Ein sichtbarer Fokusring ist dafür Pflicht, nicht optional.


/* Visually hidden sentinel elements at the start and end of the
   modal, used as an additional signal that focus is about to
   leave the trapped area via Tab or Shift+Tab */
.focus-trap-sentinel {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Make the current focus target unmistakable inside the trap */
.modal-content :focus-visible {
  outline: 3px solid #71717a;
  outline-offset: 2px;
}

/* Never rely on outline: none without a visible replacement */
.modal-content button:focus {
  outline: 3px solid #71717a;
}

8. Automatisiertes Testing mit axe-core und Playwright

Automatisierte Tools wie axe-core erkennen viele Barrierefreiheitsprobleme zuverlässig, einen Keyboard Trap jedoch nur indirekt, weil das Fehlverhalten erst durch echte Tastaturinteraktion über mehrere Schritte hinweg sichtbar wird und nicht durch eine einzelne statische DOM-Analyse. Der zuverlässigste Weg, Keyboard Traps automatisiert zu erkennen, ist deshalb ein End-to-End-Test, der mit Playwright echte Tab- und Escape-Tastendrücke simuliert und nach jedem Schritt prüft, ob document.activeElement noch innerhalb des erwarteten Containers liegt.

Ein solcher Test kombiniert idealerweise beide Ebenen: axe-core prüft strukturelle Voraussetzungen wie korrekte ARIA-Rollen und einen benannten Dialog, während der eigentliche Tastaturablauf mit Playwright das dynamische Verhalten absichert. In eine CI-Pipeline integriert, verhindert dieser Test zuverlässig, dass ein zukünftiges Refactoring des Modal-Codes unbemerkt einen neuen Keyboard Trap einführt, der sonst erst durch manuelle Nutzerbeschwerden auffallen würde.


import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('modal has no keyboard trap and returns focus on Escape', async ({ page }) => {
  await page.goto('/newsletter');
  const trigger = page.getByRole('button', { name: 'Sonderangebot anzeigen' });
  await trigger.focus();
  await trigger.press('Enter');

  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();

  // Tab through the entire dialog twice; focus must never leave it
  for (let i = 0; i < 12; i++) {
    await page.keyboard.press('Tab');
    const activeInDialog = await dialog.evaluate(
      (el, active) => el.contains(active),
      await page.evaluateHandle(() => document.activeElement)
    );
    expect(activeInDialog).toBe(true);
  }

  // Escape must always close the dialog and return focus
  await page.keyboard.press('Escape');
  await expect(dialog).toBeHidden();
  await expect(trigger).toBeFocused();

  // axe-core catches structural issues, but not the trap itself
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

9. Keyboard Traps im direkten Vergleich

Die folgende Übersicht fasst die häufigsten Trap-Szenarien zusammen und stellt jeweils das fehleranfällige dem korrekten Pattern gegenüber. Sie eignet sich als schnelle Checkliste beim Code-Review von Modal-, Dropdown- und Widget-Komponenten.

Szenario Fehleranfälliges Pattern Korrektes Pattern Effekt
Modal öffnen Kein aria-modal, kein initialer Fokus role="dialog" aria-modal="true" + Fokus auf erstes Element Screenreader kündigt Dialog korrekt an
Tab am Ende des Dialogs Fokus verschwindet im Hintergrundinhalt Zyklus zurück zum ersten Element Fokus bleibt sichtbar im Dialog
Escape-Taste Kein globaler Handler registriert Listener mit capture: true auf document Nutzer kann jederzeit aussteigen
Dialog schließen Fokus fällt auf body oder verschwindet Fokus kehrt zum auslösenden Element zurück Kontext bleibt für den Nutzer erhalten
Custom Dropdown Tab wird wie eine Pfeiltaste abgefangen Nur Pfeiltasten navigieren, Tab verlässt das Widget Erwartbares Tastaturverhalten bleibt erhalten

In der Praxis treten mehrere dieser Fehler häufig gemeinsam auf, weil sie aus derselben Ursache stammen: Fokuslogik wird nachträglich in bestehenden Code eingebaut, statt von Anfang an als vollständiges System aus Fangen, Zyklus, Escape und Rückgabe geplant zu werden. Wer die Tabelle als Checkliste bei jedem neuen interaktiven Widget durchgeht, vermeidet die überwiegende Mehrheit aller Keyboard Traps, bevor sie überhaupt in Produktion gehen.

Mironsoft

Barrierefreiheit, Fokus-Management und Accessibility-Testing für Magento- und Hyvä-Shops

Keyboard Traps zuverlässig aufspüren und beheben?

Wir testen Modals, Dropdowns und Third-Party-Widgets eures Magento- oder Hyvä-Shops systematisch mit Tastatur und Screenreader, identifizieren Fokus-Fallen und setzen robuste Fokus-Trap-Implementierungen inklusive Escape-Ausstieg um.

Keyboard-Audit

Manuelle Tastaturprüfung aller Modals, Dropdowns und eingebetteten Widgets

Fokus-Trap-Fixes

Robuste FocusTrap-Komponenten mit Escape-Ausstieg in Hyvä-Templates nachrüsten

CI-Integration

Playwright- und axe-core-Tests gegen Regressionen in der Deployment-Pipeline

10. Zusammenfassung

Ein Keyboard Trap entsteht fast immer aus fehlender oder nachträglich eingebauter Fokuslogik in Modals, Third-Party-Widgets oder Custom Dropdowns, die den Tastaturfokus binden, ohne einen zuverlässigen Ausstieg über Escape oder Tab bereitzustellen. Systematisches Testen ausschließlich mit der Tastatur deckt diese Fehler in wenigen Minuten pro Komponente auf, deutlich schneller als die meisten anderen Barrierefreiheitsprobleme. Eine korrekte Fokus-Trap-Implementierung braucht drei feste Bausteine: eine dynamisch ermittelte Liste fokussierbarer Elemente, eine Zyklus-Logik zwischen erstem und letztem Element sowie einen globalen Escape-Handler, der den Fokus zuverlässig zum auslösenden Element zurückgibt.

Automatisierte Tools wie axe-core prüfen die strukturellen Voraussetzungen, ersetzen aber keinen echten Tastaturtest, weil ein Keyboard Trap erst durch mehrschrittige Interaktion sichtbar wird. Wer eine Playwright-Testsuite gegen genau diese drei Fälle aufbaut, Öffnen und bis zum Ende tabben, sofortiges Escape und Umschalt Tab vom ersten Element, verhindert dauerhaft, dass zukünftige Refactorings unbemerkt neue Fallen in Produktion bringen.

Keyboard Traps erkennen und vermeiden, das Wichtigste auf einen Blick

Definition & WCAG

WCAG 2.1.2 „Kein Keyboard Trap", Level A. Fokus wandert hinein, kommt aber mit Tab, Escape oder Pfeiltasten nicht mehr heraus.

Häufigste Ursachen

Defekte Modal-Implementierungen, Third-Party-Widgets in iframes und Custom Dropdowns, die Tab wie eine Pfeiltaste behandeln.

Testmethode

Maus weglegen, jede Komponente per Tab, Umschalt Tab und Escape durchgehen. Drei Fälle pro Komponente prüfen.

Korrektes Pattern

Dynamische Fokusliste, Zyklus zwischen erstem und letztem Element, globaler Escape-Handler mit Fokus-Rückgabe.

11. FAQ: Keyboard Traps erkennen und vermeiden

1Was genau ist ein Keyboard Trap?
Der Tastaturfokus wandert in eine Komponente hinein und kann sie mit Tab, Umschalt Tab, Pfeiltasten oder Escape nicht mehr verlassen. Betroffene Nutzer sind bis zum Neuladen der Seite blockiert.
2Welches WCAG-Erfolgskriterium behandelt Keyboard Traps?
WCAG 2.1.2, Kein Keyboard Trap, Konformitätsstufe A. Eine der grundlegendsten Anforderungen der gesamten WCAG, da ohne sie eine Seite für Tastaturnutzer unbedienbar sein kann.
3Wie erkenne ich einen Keyboard Trap manuell?
Maus weglegen, jede Komponente per Tab, Umschalt Tab, Enter, Leertaste, Pfeiltasten und Escape bedienen. Mehrfach Tab drücken und prüfen, ob der Fokus wieder herauskommt.
4Warum verursachen Modals so häufig Keyboard Traps?
Fehlende Zyklus-Logik zwischen erstem und letztem Element sowie ein fehlender Escape-Handler halten den Fokus dauerhaft in einem einzelnen Feld gefangen.
5Was macht Third-Party-Widgets besonders riskant?
Läuft das Widget in einem iframe, endet die eigene Fokuskontrolle an der Dokumentgrenze. Fehlerhaftes Tab-Handling im iframe lässt sich von außen kaum reparieren.
6Wie baue ich eine korrekte Fokus-Trap-Implementierung?
Dynamisch ermittelte Fokusliste, Zyklus zwischen erstem und letztem Element und ein globaler Escape-Handler, der unabhängig vom aktuellen Fokus greift.
7Warum muss Escape immer funktionieren?
Der Nutzer braucht einen garantierten Ausweg. Ein Listener mit capture: true auf document stellt sicher, dass Escape ausgelöst wird, bevor verschachtelte Komponenten das Ereignis stoppen.
8Wohin soll der Fokus nach dem Schließen zurückkehren?
Zum auslösenden Element, meist dem Button, der das Modal geöffnet hat. Ein Rückfall auf body lässt den Nutzer den Kontext verlieren.
9Wie automatisiere ich Tests auf Keyboard Traps?
Mit Playwright, das echte Tab- und Escape-Tastendrücke simuliert und document.activeElement nach jedem Schritt prüft. So werden Regressionen in der CI-Pipeline abgefangen.
10Kann axe-core Keyboard Traps automatisch erkennen?
Nur indirekt über strukturelle Voraussetzungen. Der eigentliche Trap wird erst durch mehrschrittige Interaktion sichtbar, deshalb braucht es zusätzlich echte Tastaturtests mit Playwright.