Focus Trap in JavaScript

Den Dialogfokus führen und wieder freigeben

Öffnen, Erstfokus, Schließen: drei Übergänge im Dialog.

Website prüfen
Beispiel im Prüfbericht Markierter Befund
Markierter Befund · Kritisch · WCAG 4.1.2Erreichbarer Link im verborgenen Bereich

Fokus im richtigen Raum

Ein Dialog ist ein geschlossener Fokusweg

Beim Öffnen hinein, mit Tab im Kreis, beim Schließen zurück.

Fokus-Orbit eines modalen DialogsKonzeptdarstellung, keine echte Bedienoberfläche
Fokus läuft im Dialog und kehrt danach zum Auslöser zurück Der Fokus startet auf dem Auslöser. Nach dem Öffnen steht er auf Abbrechen. Tab bewegt ihn zu Termin absagen und vom letzten wieder zum ersten Element. Escape schließt den Dialog und führt zum Auslöser zurück.
Modales FensterTermin wirklich absagen?

Der Termin wird aus Ihrem Kalender entfernt.

Abbrechen ErstfokusTermin absagen
1 Auslöser merken2 Fokus hineinsetzen3 Tab und Shift+Tab bleiben innen4 Schließen und zurückgeben
  1. 1Auslöser „Termin absagen“ erhält den Fokus.
  2. 2Öffnen setzt den Fokus auf „Abbrechen“.
  3. 3Tab-Kreis verbindet beide Dialogaktionen.
  4. 4Escape schließt und führt zum Auslöser zurück.

Empfohlener Weg

Lassen Sie showModal() den Fang übernehmen

Ein modal geöffnetes dialog legt den Hintergrund still, hält den Fokus innen und unterstützt Escape. Weil die Aktion nicht leicht rückgängig ist, erhält „Abbrechen“ hier den Erstfokus.

Fokus hinein beim ÖffnenTab-Kreis im DialogHintergrund inert solange offenFokus zurück nach dem Schließen
<button type="button" id="open-dialog">
  Termin absagen
</button>

<dialog id="confirm-dialog"
  aria-labelledby="confirm-title">
  <h2 id="confirm-title">
    Termin wirklich absagen?
  </h2>
  <p>Der Termin wird aus Ihrem Kalender entfernt.</p>
  <button type="button" id="cancel" autofocus>
    Abbrechen
  </button>
  <button type="button" id="confirm">
    Termin absagen
  </button>
</dialog>
const dialog = document.querySelector('#confirm-dialog');
const open = document.querySelector('#open-dialog');
const cancel = document.querySelector('#cancel');
const confirm = document.querySelector('#confirm');

open.addEventListener('click', () => dialog.showModal());
cancel.addEventListener('click', () => dialog.close('cancel'));
confirm.addEventListener('click', () => dialog.close('confirm'));

Vier Zustände

An den Grenzen zeigt sich, ob der Fokusweg stimmt

TabAbbrechen → Absagen

Der Fokus folgt der Reihenfolge im Dialog.

TabLetztes → Erstes

Am Ende beginnt der Dialogkreis von vorn.

Shift + TabErstes → Letztes

Auch rückwärts bleibt der Fokus im Dialog.

EscDialog → Auslöser

Der Dialog schließt und der Arbeitsweg geht weiter.

Nur für eigene Modalfenster

Wenn kein dialog-Element möglich ist

Der eigene Fang braucht mehr als zwei Randabfragen. Der Dialog steht außerhalb des inerten Seitenbereichs, liest seine aktiven Ziele bei jedem Tab neu und entfernt seinen Listener beim Schließen.

  • Hintergrund mit inert stilllegen
  • sichtbare, aktive Fokusziele neu ermitteln
  • Tab an beiden Grenzen umlenken
  • Escape, Aufräumen und Rückfokus verbinden

Das Beispiel setzt einen einzelnen Dialog im normalen DOM voraus. Verschachtelte Dialoge oder Shadow DOM brauchen eine eigene Strategie.

const page = document.querySelector('#page');
const dialog = document.querySelector('#custom-dialog');
const open = document.querySelector('#open-custom');
const close = document.querySelector('#close-custom');
const focusable = [
  'a[href]',
  'button:not(:disabled)',
  'input:not(:disabled)',
  'select:not(:disabled)',
  'textarea:not(:disabled)',
  '[tabindex]:not([tabindex="-1"])'
].join(',');

function openDialog() {
  dialog.hidden = false;
  page.inert = true;
  dialog.addEventListener('keydown', keepFocus);
  (dialog.querySelector('[autofocus]') ?? dialog).focus();
}

function keepFocus(event) {
  if (event.key === 'Escape') return closeDialog();
  if (event.key !== 'Tab') return;

  const items = [...dialog.querySelectorAll(focusable)]
    .filter((item) => item.getClientRects().length);
  const first = items[0];
  const last = items.at(-1);

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

function closeDialog() {
  dialog.removeEventListener('keydown', keepFocus);
  dialog.hidden = true;
  page.inert = false;
  open.focus();
}

open.addEventListener('click', openDialog);
close.addEventListener('click', closeDialog);

Die nötige Struktur zum Fallback

<main id="page">
  <button type="button" id="open-custom">Hinweis öffnen</button>
</main>

<section id="custom-dialog" role="dialog"
  aria-modal="true" aria-labelledby="custom-dialog-title"
  tabindex="-1" hidden>
  <h2 id="custom-dialog-title">Wichtiger Hinweis</h2>
  <p>Lesen Sie den Hinweis vor dem Fortfahren.</p>
  <button type="button" id="close-custom" autofocus>
    Schließen
  </button>
</section>

So prüfen Sie es

Testen Sie auf der fertigen Seite, nicht nur im Komponenten-Preview.

  1. Öffnen: Der Fokus steht sichtbar im Dialog.
  2. Tab: Nach dem letzten Ziel folgt das erste.
  3. Shift + Tab: Vor dem ersten folgt das letzte.
  4. Escape: Der Dialog schließt ohne Maus.
  5. Rückweg: Der Fokus steht wieder am Auslöser.

Aus dem Prüfbericht

Zwei Befunde am offenen Dialog

Kritisch Der Hintergrund bleibt hinter dem Dialog erreichbar Regel aria-hidden-focus

Das Skript setzt aria-hidden auf die Seite, die Links dahinter bleiben aber in der Tab-Reihenfolge. showModal() macht den Hintergrund eines dialog-Elements stattdessen automatisch inert.

Anleitung: Fokussierbare Elemente nicht mit ARIA verstecken

Zum Mitnehmen

Der Dialog im Skript, korrigiert

Ein Attribut, ein Name, ein Ereignis für den Rückweg.

Korrektur in 3 Schritten ansehen
  1. Den Dialog wirklich modal öffnen

    show() zeigt einen nicht-modalen Dialog. showModal() legt den Hintergrund automatisch still und hält den Fokus im dialog-Element.

    KorrekturvergleichSchritt 1 · JavaScript

    Vorher

    function oeffnen() {
      dialog.show();
    }

    Nachher

    function oeffnen() {
      dialog.showModal();
    }

    Markierte Zeilen wurden geändert

    So prüfen Sie es Drücken Sie im offenen Dialog mehrmals die Tabulatortaste. Kein Fokus landet dahinter.

  2. Dem Schließen-Kreuz einen Namen geben

    Das Kreuz bleibt sichtbar und für Vorleseprogramme verborgen. aria-label liefert den Namen, den die Schaltfläche im Vorleseprogramm bekommt.

    KorrekturvergleichSchritt 2 · HTML

    Vorher

    <button type="button" class="dialog-schliessen">
      <span aria-hidden="true">✕</span>
    </button>

    Nachher

    <button type="button" class="dialog-schliessen" aria-label="Dialog schließen">
      <span aria-hidden="true">✕</span>
    </button>

    Markierte Zeilen wurden geändert

    So prüfen Sie es Gehen Sie mit der Tabulatortaste auf das Kreuz. Ein Vorleseprogramm sagt „Dialog schließen, Schaltfläche“.

  3. Den Rückfokus ausdrücklich setzen

    Der Browser gibt den Fokus normalerweise selbst zurück. Ein close-Listener macht den vorgesehenen Rückweg ausdrücklich und kann bei Bedarf ein anderes logisches Ziel wählen.

    KorrekturvergleichSchritt 3 · JavaScript

    Vorher

    oeffner.addEventListener('click', () => oeffnen());

    Nachher

    oeffner.addEventListener('click', () => oeffnen());
    dialog.addEventListener('close', () => {
      oeffner.focus();
    });

    Markierte Zeilen wurden geändert

    So prüfen Sie es Öffnen Sie den Dialog und drücken Sie Escape. Der Fokus steht wieder auf dem Auslöser.

Werbung

Kostenloser Website-Barrierefreiheits-Check

Website auf Barrierefreiheit prüfen.

Technische WCAG-Warnsignale erkennen. Kritische Stellen zuerst prüfen.

Der Website-Check ist bereit.

Werbung

Ein letzter Schritt

Prüfen, bevor Sie gehen.

Finden Sie automatisch erkennbare Barrieren auf Ihrer eigenen Website.

Website prüfen

Kostenlos starten · keine Installation

Automatisches Screening. Tastaturbedienung und Screenreader bleiben manuelle Prüfung.