Focus Trap in JavaScript
Den Dialogfokus führen und wieder freigeben
Öffnen, Erstfokus, Schließen: drei Übergänge im Dialog.
Fokus im richtigen Raum
Ein Dialog ist ein geschlossener Fokusweg
Beim Öffnen hinein, mit Tab im Kreis, beim Schließen zurück.
Der Termin wird aus Ihrem Kalender entfernt.
- 1Auslöser „Termin absagen“ erhält den Fokus.
- 2Öffnen setzt den Fokus auf „Abbrechen“.
- 3Tab-Kreis verbindet beide Dialogaktionen.
- 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.
<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
Der Fokus folgt der Reihenfolge im Dialog.
Am Ende beginnt der Dialogkreis von vorn.
Auch rückwärts bleibt der Fokus im Dialog.
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
inertstilllegen - 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.
- Öffnen: Der Fokus steht sichtbar im Dialog.
- Tab: Nach dem letzten Ziel folgt das erste.
- Shift + Tab: Vor dem ersten folgt das letzte.
- Escape: Der Dialog schließt ohne Maus.
- Rückweg: Der Fokus steht wieder am Auslöser.
Aus dem Prüfbericht
Zwei Befunde am offenen Dialog
Kritisch Das Kreuz im Dialog hat keinen Namen Regel button-name
Die Schaltfläche zum Schließen enthält nur ein Symbol. aria-label="Dialog schließen" nennt die Aktion im Vorleseprogramm.
Zum Mitnehmen
Der Dialog im Skript, korrigiert
Ein Attribut, ein Name, ein Ereignis für den Rückweg.
Korrektur in 3 Schritten ansehen
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.
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“.
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.
