Modal-Dialoge barrierefrei: Focus-Trap richtig implementieren
AI generated
A11Y
WCAG
Barrierefreiheit
Modal-Dialoge barrierefrei
Focus-Trap richtig implementieren

Ein Modal, das optisch alles überlagert, aber den Tastaturfokus frei durch die dahinterliegende Seite wandern lässt, ist für Tastatur- und Screenreader-Nutzer keine wirkliche Überlagerung. Sie tabben durch Navigation, Header und Footer der Hintergrundseite, während der Dialog optisch im Vordergrund schwebt, unsichtbar für den eigenen Fokus-Cursor. Ein korrekt implementierter Focus-Trap sorgt dafür, dass der Tastaturfokus exakt dort bleibt, wo er visuell auch erscheint: innerhalb des Dialogs, bis dieser bewusst geschlossen wird.

10 Min. Lesezeit Focus Trap role="dialog" aria-modal

1. Warum der Fokus im Modal gefangen werden muss

Sobald ein Modal geöffnet wird, ändert sich für sehende Maus-Nutzer die gesamte Interaktionsfläche: Alles außerhalb des Dialogs ist visuell abgedunkelt oder durch einen Overlay-Hintergrund optisch deaktiviert, oft zusätzlich mit pointer-events: none unterlegt. Ohne einen Focus-Trap bleibt die Tab-Reihenfolge des darunterliegenden Dokuments jedoch vollständig intakt. Ein Tastaturnutzer, der nach dem Öffnen eines Newsletter-Popups weiter Tab drückt, springt unsichtbar durch Links und Buttons der Hintergrundseite, ohne zu wissen, dass er sich außerhalb des sichtbaren Dialogs befindet.

Für Screenreader-Nutzer verschärft sich das Problem noch: Ohne technische Absicherung liest der Screenreader unter Umständen Inhalte der Hintergrundseite vor, während der Dialog geöffnet ist, was vollkommen unverständlich wirkt, weil der angekündigte Kontext, etwa 'Dialog: Newsletter abonnieren', nicht zu dem passt, was tatsächlich vorgelesen wird. Ein Focus-Trap löst beide Probleme gleichzeitig: Er begrenzt die Tab-Reihenfolge auf die fokussierbaren Elemente innerhalb des Dialogs und verhindert, dass der Fokus unbemerkt in den Hintergrund abrutscht.

2. Korrekte ARIA-Attribute: role="dialog", aria-modal und aria-labelledby

Ein barrierefreier Dialog braucht mindestens drei ARIA-Bausteine, die zusammenwirken. Erstens role="dialog" auf dem Wrapper-Element, das dem Accessibility Tree mitteilt, dass es sich um einen eigenständigen Dialog handelt, nicht um regulären Seiteninhalt. Zweitens aria-modal="true", das Screenreadern explizit signalisiert, dass der Inhalt außerhalb des Dialogs während der Dialog-Anzeige nicht relevant ist, wodurch moderne Screenreader den Hintergrund automatisch aus der Navigation ausschließen. Drittens aria-labelledby, das auf die ID der sichtbaren Dialog-Überschrift verweist und so beim Öffnen sofort ansagt, worum es in diesem Dialog geht.

Diese drei Attribute ersetzen keinen funktionierenden Focus-Trap, sie ergänzen ihn. aria-modal="true" beeinflusst zwar die Screenreader-Navigation, ändert aber nichts an der tatsächlichen Tab-Reihenfolge im DOM. Ein Dialog mit perfekten ARIA-Attributen, aber ohne JavaScript-Focus-Trap, bleibt für sehende Tastaturnutzer weiterhin fehlerhaft bedienbar, auch wenn Screenreader-Nutzer durch aria-modal teilweise abgesichert sind.


<div x-show="open"
     role="dialog"
     aria-modal="true"
     aria-labelledby="modal-title"
     class="fixed inset-0 z-50 flex items-center justify-center">
  <div class="bg-white rounded-xl p-6 max-w-md w-full" @keydown.escape.window="close()">
    <h2 id="modal-title" class="text-lg font-semibold">Newsletter abonnieren</h2>
    <!-- Dialog-Inhalt -->
  </div>
</div>

3. Focus-Trap in Alpine.js implementieren

Hyvä setzt standardmäßig auf Alpine.js, das kein eingebautes Focus-Trap-Verhalten mitbringt, es aber über das offizielle @alpinejs/focus-Plugin nachrüsten lässt. Das Plugin stellt die x-trap-Direktive bereit, die bei Aktivierung automatisch den ersten fokussierbaren Nachfahren fokussiert, die Tab-Reihenfolge auf den Dialog begrenzt und bei Deaktivierung den Fokus auf das Element zurückgibt, das vor dem Öffnen fokussiert war.

Wichtig ist die korrekte Bindung an den offenen Zustand des Modals: x-trap="open" aktiviert die Falle, sobald die Alpine-Variable open wahr wird, und deaktiviert sie automatisch beim Schließen. Ohne das Plugin müsste dieselbe Logik manuell über einen keydown-Listener implementiert werden, der bei Tab und Shift+Tab prüfen muss, ob der Fokus das erste oder letzte fokussierbare Element im Dialog verlässt, und ihn in diesem Fall zyklisch zurückspringen lässt.


// app.js: Alpine-Focus-Plugin registrieren
import Alpine from 'alpinejs';
import focus from '@alpinejs/focus';

Alpine.plugin(focus);
Alpine.start();

4. Vollständige Hyvä-Modal-Komponente mit x-trap

Eine vollständige Implementierung kombiniert x-trap mit den ARIA-Attributen aus dem vorherigen Abschnitt sowie einem ESC-Handler. Entscheidend ist, dass x-trap direkt an das Container-Element gebunden wird, das alle fokussierbaren Elemente des Dialogs umschließt, nicht an ein äußeres Overlay-Element, das selbst nicht fokussierbar ist.

In diesem Beispiel wird außerdem der Hintergrund per @click.self als Schliess-Trigger genutzt, was für Maus-Nutzer intuitiv ist, aber niemals der einzige Schliess-Mechanismus sein darf: Sowohl ein sichtbarer Schließen-Button als auch die ESC-Taste müssen redundant vorhanden sein, damit Tastaturnutzer den Dialog unabhängig vom Klick-auf-Hintergrund-Verhalten beenden können.


<!-- Hyvä: modal.phtml, vollständige Focus-Trap-Implementierung -->
<div x-data="{ open: false }" @open-modal.window="open = true">
  <div x-show="open"
       x-trap="open"
       @click.self="open = false"
       @keydown.escape.window="open = false"
       role="dialog"
       aria-modal="true"
       aria-labelledby="modal-title"
       class="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
    <div class="bg-white rounded-xl p-6 max-w-md w-full">
      <div class="flex items-center justify-between mb-4">
        <h2 id="modal-title" class="text-lg font-semibold">Newsletter abonnieren</h2>
        <button type="button" @click="open = false" aria-label="Dialog schließen"
                class="min-h-[24px] min-w-[24px]">
          <svg class="h-5 w-5" aria-hidden="true"><!-- X-Icon --></svg>
        </button>
      </div>
      <!-- Formularinhalt -->
    </div>
  </div>
</div>

5. Fokus-Rückgabe beim Schließen: der am häufigsten vergessene Schritt

Ein funktionierender Focus-Trap innerhalb des Dialogs löst nur die Hälfte des Problems. Beim Schließen muss der Fokus explizit auf das Element zurückgesetzt werden, das den Dialog ursprünglich geöffnet hat, meist ein Button. Ohne diese Rückgabe fällt der Fokus im Browser standardmäßig auf body zurück, was für Tastaturnutzer bedeutet, dass sie nach dem Schließen erneut von ganz vorne durch die Seite tabben müssen, um zur ursprünglichen Position zurückzufinden.

Das Alpine-Focus-Plugin übernimmt diese Rückgabe bei korrekter Nutzung von x-trap automatisch, sofern der Trigger-Button zum Zeitpunkt des Öffnens tatsächlich fokussiert war. Wird der Dialog hingegen programmatisch geöffnet, etwa nach einem AJAX-Request ohne vorherigen Button-Klick, muss die ursprüngliche Fokus-Referenz manuell gespeichert und beim Schließen wiederhergestellt werden.


// Manuelle Fokus-Rückgabe für Dialoge, die ohne direkten
// Button-Klick geöffnet werden, z.B. nach einem AJAX-Response
function programmaticModal() {
  return {
    open: false,
    lastFocusedElement: null,
    show() {
      this.lastFocusedElement = document.activeElement;
      this.open = true;
    },
    hide() {
      this.open = false;
      this.$nextTick(() => this.lastFocusedElement?.focus());
    },
  };
}

6. ESC-Taste: Pflichtfunktion, kein Nice-to-have

Die ESC-Taste als Schliess-Mechanismus ist für Tastaturnutzer nicht optional. Ohne sie muss ein Nutzer erst per Tab durch alle fokussierbaren Elemente des Dialogs navigieren, um einen Schließen-Button zu erreichen, was besonders bei längeren Formularen innerhalb eines Modals unnötig mühsam ist. Die WAI-ARIA Authoring Practices führen ESC explizit als erwartetes Tastaturverhalten für das Dialog-Pattern.

Ein häufiger Bug in Alpine.js-Implementierungen: Der @keydown.escape-Listener wird direkt auf das Dialog-Element statt auf window gebunden. Befindet sich der Fokus dann auf einem Formularfeld innerhalb des Dialogs, das selbst auf Tastendrücke reagiert, etwa ein natives select-Element, kann das ESC-Ereignis je nach Browser nicht zuverlässig beim Dialog-Element ankommen. Die Bindung an @keydown.escape.window fängt das Ereignis global ab und funktioniert unabhängig davon, welches Kindelement gerade fokussiert ist.

7. Typische Bugs bei Alpine.js-Modals im Hyvä-Kontext

Vier Fehler tauchen in Hyvä-Projekten besonders regelmäßig auf. Erstens: x-trap wird an ein Element gebunden, das per x-show aus dem Layout entfernt wird, bevor Alpine den initialen Fokus setzen konnte, was zu einem Timing-Bug führt, bei dem der Fokus scheinbar zufällig manchmal funktioniert und manchmal nicht. Die Lösung ist, x-trap und x-show konsistent an dieselbe Bedingung zu binden und keine zusätzliche Verzögerung durch CSS-Transitions einzubauen, die das DOM erst nach der Fokussierung sichtbar machen.

Zweitens: Mehrere gleichzeitig im DOM vorhandene Modals, von denen mehrere versehentlich x-trap aktiv haben, weil der zugrunde liegende Alpine-State nicht sauber pro Modal isoliert wurde. Drittens: Der Mini-Cart-Slide-Over, der in vielen Hyvä-Themes technisch kein role="dialog" trägt, obwohl er sich wie ein Modal verhält und den Hintergrund abdunkelt, wodurch Screenreader-Nutzer nicht erfahren, dass sie sich in einem eigenständigen Kontext befinden. Viertens: Der Fokus wird beim Öffnen zwar korrekt in den Dialog gesetzt, aber auf ein falsches Element, etwa den Dialog-Container selbst statt auf das erste sinnvolle interaktive Element oder die Überschrift, was Screenreader-Nutzern keinen klaren Einstiegspunkt bietet.

8. Verschachtelte Modals und dynamischer Inhalt vermeiden

Ein zweites Modal, das aus einem bereits geöffneten Modal heraus geöffnet wird, etwa eine Bestätigungs-Nachfrage innerhalb eines Formular-Dialogs, verkompliziert Focus-Trap-Logik erheblich, weil zwei verschachtelte Traps gleichzeitig aktiv verwaltet werden müssen und die Fokus-Rückgabe beim Schließen des inneren Dialogs korrekt zum äußeren Dialog zurückführen muss, nicht zur ursprünglichen Seite dahinter.

Wo immer möglich, lohnt es sich, verschachtelte Modals architektonisch zu vermeiden und stattdessen den Inhalt des bestehenden Dialogs dynamisch auszutauschen, etwa über einen zweistufigen Alpine-State statt eines zweiten physischen Dialog-Elements. Das reduziert nicht nur die Komplexität der Focus-Trap-Logik, sondern verbessert auch die Screenreader-Erfahrung, da nur eine einzige Dialog-Ankündigung pro Interaktion erfolgt.

9. Test-Checkliste für barrierefreie Modals

Sechs Prüfschritte lassen sich in jedem Review durchgehen: Modal per Tastatur öffnen und prüfen, dass der Fokus automatisch in den Dialog springt. Innerhalb des Dialogs mehrfach Tab drücken und sicherstellen, dass der Fokus nie in den Hintergrund entweicht, sondern am Ende des Dialogs zum Anfang zurückspringt. Shift+Tab vom ersten Element aus testen, um zu prüfen, ob die Falle auch rückwärts korrekt schließt.

ESC-Taste drücken und prüfen, dass sich der Dialog unabhängig vom aktuell fokussierten Kindelement schließt. Nach dem Schließen kontrollieren, ob der Fokus zuverlässig zum ursprünglichen Trigger-Element zurückkehrt. Abschließend mit einem aktiven Screenreader prüfen, ob beim Öffnen die Dialog-Überschrift korrekt angesagt wird und der Hintergrundinhalt während der Dialog-Anzeige nicht vorgelesen wird.

Element Zweck Häufiger Fehler Korrektur
role="dialog" Markiert den Container als eigenständigen Dialog Fehlt komplett bei Slide-Over-Komponenten wie Mini-Cart Auf jedem modalen Overlay ergänzen
aria-modal="true" Schließt Hintergrundinhalt aus der Screenreader-Navigation aus Vorhanden, aber ohne funktionierenden Focus-Trap Immer in Kombination mit x-trap einsetzen
x-trap (Alpine-Plugin) Hält Tab-Fokus innerhalb des Dialogs An Element gebunden, das per x-show entfernt wird x-trap und x-show an dieselbe Bedingung binden
@keydown.escape.window Schließt den Dialog per ESC-Taste Nur lokal am Dialog-Element gebunden statt an window Immer .window-Modifier verwenden
Fokus-Rückgabe Setzt Fokus nach Schließen auf Trigger-Element zurück Fokus fällt auf body zurück lastFocusedElement speichern und zurückfokussieren

Mironsoft

WCAG-Audits, barrierefreie Magento-Shops und Schulungen

Unsicher, ob der Shop wirklich barrierefrei ist?

Wir prüfen bestehende Magento-Shops gegen WCAG 2.2, beheben konkrete Barrieren im Hyvä-Frontend und schulen Teams, damit Barrierefreiheit dauerhaft im Entwicklungsprozess verankert bleibt.

WCAG-Audit

Shop systematisch gegen WCAG 2.2 AA prüfen, mit priorisierter Fehlerliste.

Barrieren beheben

Konkrete Umsetzung: Tastaturbedienbarkeit, Screenreader-Support, Kontraste, Formulare.

Team-Schulung

Entwickler und Redakteure für barrierefreie Umsetzung im Alltag sensibilisieren.

10. Zusammenfassung

Modal-Focus-Trap: Das Wichtigste auf einen Blick

Kernproblem

Ohne Focus-Trap bleibt die Tab-Reihenfolge des Hintergrunds aktiv, wodurch Tastaturnutzer unsichtbar hinter den geöffneten Dialog fallen.

ARIA-Basis

role="dialog", aria-modal="true" und aria-labelledby bilden die semantische Grundlage, ersetzen aber keinen echten JavaScript-Focus-Trap.

Alpine-Lösung

Das offizielle @alpinejs/focus-Plugin mit x-trap übernimmt Fokus-Einfang und Fokus-Rückgabe automatisch bei korrekter Bindung an den Modal-Zustand.

Häufigster Bug

ESC-Handler wird lokal statt mit .window-Modifier gebunden, wodurch das Schließen bei fokussierten Formularelementen unzuverlässig wird.

11. FAQ: Modal-Focus-Trap: Das Wichtigste auf einen Blick

1Reicht aria-modal allein, um einen Dialog barrierefrei zu machen?
Nein, aria-modal beeinflusst nur die Screenreader-Navigation, ändert aber nichts an der tatsächlichen Tab-Reihenfolge im DOM. Ein funktionierender JavaScript-Focus-Trap ist zusätzlich zwingend erforderlich.
2Welches Alpine.js-Plugin brauche ich für Focus-Trap-Funktionalität?
Das offizielle @alpinejs/focus-Plugin stellt die x-trap-Direktive bereit, die Fokus-Einfang, zyklisches Tabben innerhalb des Dialogs und Fokus-Rückgabe beim Schließen automatisch übernimmt.
3Warum fällt der Fokus manchmal auf body statt auf das Trigger-Element zurück?
Das passiert, wenn die Fokus-Rückgabe nicht implementiert ist. Ohne explizite Speicherung des zuvor fokussierten Elements setzt der Browser den Fokus beim Entfernen eines fokussierten Elements standardmäßig auf body zurück.
4Warum sollte der ESC-Handler an window statt am Dialog-Element gebunden werden?
Ist der Fokus auf einem Formularelement innerhalb des Dialogs, das selbst auf Tastendrücke reagiert, etwa ein natives select, kommt das ESC-Ereignis sonst je nach Browser nicht zuverlässig beim Dialog-Element an. Die Bindung an window fängt das Ereignis global ab.
5Ist der Mini-Cart-Slide-Over in Hyvä ein echtes Modal?
Funktional oft ja, technisch aber häufig nicht korrekt ausgezeichnet, weil role="dialog" und aria-modal fehlen, obwohl der Hintergrund abgedunkelt und interaktionslos gemacht wird. Das sollte in Themes explizit nachgerüstet werden.
6Wie teste ich einen Focus-Trap ohne Screenreader?
Maus vollständig weglegen, Dialog per Tastatur öffnen und mehrfach Tab sowie Shift+Tab drücken. Der Fokus darf dabei niemals in den Hintergrund entweichen und muss am Ende des Dialogs zyklisch zum Anfang zurückspringen.
7Was passiert bei verschachtelten Modals mit Focus-Trap?
Zwei gleichzeitig aktive Traps müssen korrekt verwaltet werden, und die Fokus-Rückgabe beim Schließen des inneren Dialogs muss zum äußeren Dialog zurückführen, nicht zur ursprünglichen Seite. Wo möglich sollten verschachtelte Modals architektonisch vermieden werden.
8Muss die Dialog-Überschrift zwingend per aria-labelledby verknüpft sein?
Ja, ansonsten kündigt der Screenreader beim Öffnen zwar den Dialog an, aber ohne erkennbaren Titel, was Nutzer im Unklaren lässt, worum es in dem Dialog geht.
9Warum funktioniert x-trap manchmal nur unzuverlässig?
Ein häufiger Grund ist ein Timing-Bug, wenn x-trap an ein Element gebunden ist, das per x-show erst nach der initialen Fokussierung sichtbar wird. x-trap und x-show sollten konsistent an dieselbe Bedingung gebunden werden.
10Reicht ein Klick auf den Hintergrund als einziger Schliess-Mechanismus?
Nein, das Klick-auf-Hintergrund-Verhalten ist praktisch für Maus-Nutzer, darf aber niemals der einzige Weg sein. Ein sichtbarer Schließen-Button und die ESC-Taste müssen zusätzlich immer funktionieren.