Komponenten · Interaktive Widgets
Barrierefreies Modal: dialog-Element & showModal()
Ein barrierefreies Modal baust du heute mit dem nativen <dialog>-Element und der Methode showModal(): Sie hebt den Dialog in den Top-Layer, rendert einen ::backdrop, schließt per Esc, macht den Hintergrund inert und setzt aria-modal="true" implizit – Stand Juli 2026 in jedem Browser, Baseline seit März 2022. Bei dir bleiben drei Aufgaben: ein zugänglicher Name per aria-labelledby, ein sinnvoller Initialfokus per autofocus und ein echter Schließen-Button.
Fokusfalle, Esc-Handler, Scroll-Sperre, Fokus-Rückgabe: Das meiste davon übernimmt heute der Browser.
Das Wichtigste in Kürze
-
<dialog>läuft seit März 2022 in allen Browsern (zuletzt Safari 15.4): Stand Juli 2026 kein Grund für div-Eigenbauten. -
showModal()liefert automatisch: Top-Layer,::backdrop, Esc, inerten Hintergrund, implizitesaria-modal="true".show()öffnet nicht-modal – ohne all das. - Tastatur nach W3C APG: Tab und Shift+Tab zirkulieren, Esc schließt, der Fokus kehrt zum Auslöser zurück.
- Die Fokusfalle im Modal ist WCAG-konform: 2.1.2 (Stufe A) erlaubt sie, solange Esc als Ausweg funktioniert.
- Ein korrekt gebautes Modal besteht 2.4.11 „Fokus nicht verdeckt“ (Stufe AA, WCAG 2.2) laut W3C-Understanding immer.
- H102 ist die offizielle WCAG-Technik für modale Dialoge mit
dialog; Fehlermuster F85: Dialog nicht beim Auslöser in der Fokus-Reihenfolge. - Popover sind immer nicht-modal – ein gesperrter Hintergrund verlangt
<dialog>mitshowModal().
Was ein modaler Dialog leisten muss
Ein modaler Dialog blockiert die Seite dahinter und enthält immer Text plus mindestens ein Bedienelement zum Schließen – so definiert ihn die Handreichung der BFIT-Bund. Daraus folgen vier Fokus-Pflichten: den Fokus beim Öffnen hineinbewegen, ihn im Dialog halten, Esc als Ausweg anbieten und ihn beim Schließen zurückgeben. Fehlen sie, ist das Overlay für die Tastaturbedienung eine Wand mit unsichtbaren Bedienelementen, und Screenreader-Nutzer merken die Öffnung nicht.
Normativ steht das in der EN 301 549; die BFIT-Bund-Handreichung bündelt dreizehn Einzelanforderungen an modale Dialoge (dort Nr. 426–438), vom Fokus-Setzen über Rolle, Name und Beschreibung bis zur Vergrößerbarkeit. Fast alles liefert <dialog> mit.
Der Standardweg: <dialog> mit showModal()
Stand Juli 2026 hat das
<dialog>-Element laut MDN
den Baseline-Status „Widely available“, browserübergreifend seit März 2022 mit Safari 15.4.
<button type="button" id="einstellungen-oeffnen">Einstellungen</button>
<dialog id="einstellungen" aria-labelledby="einstellungen-titel">
<h2 id="einstellungen-titel">Einstellungen</h2>
<p>Sprache und Benachrichtigungen anpassen.</p>
<label for="sprache">Sprache</label>
<select id="sprache">
<option>Deutsch</option>
<option>Englisch</option>
</select>
<button type="button" id="einstellungen-schliessen" autofocus>Schließen</button>
</dialog>
const dialog = document.getElementById('einstellungen');
document.getElementById('einstellungen-oeffnen')
.addEventListener('click', () => dialog.showModal());
document.getElementById('einstellungen-schliessen')
.addEventListener('click', () => dialog.close());
Was showModal() ohne weiteren Code erledigt:
- Fokus hinein: auf das Element mit
autofocus, sonst auf das erste fokussierbare Element; beim Schließen zurück auf den Öffnen-Button. - Fokus gehalten: Der Rest des Dokuments wird inert – nicht fokussierbar, nicht klickbar, für assistive Technik unsichtbar; Tab zirkuliert im Dialog.
- Esc schließt und feuert
close; dazu kommen die implizite Rolledialogundaria-modal="true"in der Accessibility API.
Zwei MDN-Warnungen, die ich regelmäßig verletzt sehe: Das open-Attribut nie von Hand setzen – das öffnet nur nicht-modal – und tabindex gehört nie aufs dialog-Element.
show() öffnet kein Modal
show() öffnet denselben Dialog nicht-modal: kein Backdrop, kein Esc-Verhalten, keine Fokusfalle, kein modaler Status in der Accessibility API – für ein begleitendes Panel legitim, als „Modal“ die häufigste Mogelpackung. Nicht-modale Dauer-Overlays wie klebende Cookie-Banner verdecken zudem fokussierte Elemente und reißen Kriterium 2.4.11 „Fokus nicht verdeckt“, sobald sie ein fokussiertes Element vollständig verdecken – während das
Understanding-Dokument
für saubere Modals festhält: „A properly constructed modal dialog will always pass this SC“.
Rolle, Name und Beschreibung
Die Rolle bringt das Element mit, den Namen nicht: Ohne zugänglichen Namen besteht der Dialog
4.1.2 „Name, Rolle, Wert“
nicht, der Screenreader sagt nur „Dialog“ an. Standardweg ist aria-labelledby auf die sichtbare Überschrift, sonst aria-label; die Rückfrage im Bestätigungsdialog bindest du per aria-describedby an (Rollen, States & Properties).
Nicht nötig, auch wenn ältere Anleitungen es empfehlen: role="dialog" (implizite Rolle) und aria-modal="true" (setzt showModal() selbst). aria-modal ist reine Semantik und fängt keinen Fokus – das tut der Browser oder dein JavaScript.
Vor aria-modal (ARIA 1.1) musste der ganze Hintergrund aria-hidden="true" bekommen – eine Krücke, die in Altprojekten bis heute vorkommt. Der Dialog darf dann nie Nachfahre eines versteckten Containers sein; alle Bedienelemente müssen DOM-Nachfahren des Dialog-Containers sein (W3C APG).
Initialfokus und Fokus-Rückgabe
Ohne autofocus fokussiert showModal() das erste fokussierbare Element – oft richtig, nicht immer. Die Regeln aus dem W3C APG Dialog Pattern:
- Normalfall: das erste fokussierbare Element – oder per
autofocusdas, mit dem die Arbeit beginnt. - Langer Inhalt (etwa Vertragsbedingungen): ein statisches Element am Anfang fokussieren – die Überschrift mit
tabindex="-1"oder, seit einer Änderung der HTML-Spezifikation (2023), dasdialog-Element selbst perautofocus; dann beginnt die Vorlese-Reihenfolge oben und nichts wird übersprungen. - Destruktive Aktionen: nie den gefährlichen Button vorfokussieren – bei „Endgültig löschen?“ gehört der Fokus auf „Abbrechen“.
- Info-Dialoge: das wahrscheinlichste Element, meist „OK“.
Beim Schließen gibt das native Element den Fokus automatisch an den Auslöser zurück; im Eigenbau merkst du ihn dir vorher. Die BFIT-Handreichung erlaubt auch ein Element, mit dem sich die Arbeit fortsetzen lässt – nötig in den zwei APG-Ausnahmen: Der Auslöser existiert nicht mehr (der „Löschen“-Button einer entfernten Zeile), oder der Workflow führt weiter; dann fokussierst du etwa die Liste, aus der die Zeile verschwand. Landet der Fokus am Seitenanfang, ist das Fehlermuster F85 zu 2.4.3 „Fokus-Reihenfolge“.
Die Tastatur-Erwartung
| Taste | Verhalten im modalen Dialog |
|---|---|
| Tab | nächstes Element; vom letzten zum ersten |
| Shift + Tab | vorheriges Element; vom ersten zum letzten |
| Esc | schließt den Dialog, wirkt wie „Abbrechen“ |
Diese Zirkulation ist eine erlaubte Fokusfalle: 2.1.2 „Keine Tastaturfalle“ (Stufe A) gestattet die Beschränkung auf einen Seitenbereich ausdrücklich – solange ein Standard-Ausweg wie Esc existiert oder erklärt wird. Ohne Ausweg ist es Fehlermuster F10.
Zwei Sonderfälle: Bei verschachtelten Dialogen schließt Esc nur den obersten Dialog im Top-Layer. Und Esc löst erst cancel aus, dann close – dialog.close() per Button nur close.
Bestätigen ohne Event-Spaghetti: form method="dialog"
Für Bestätigungsdialoge bringt HTML einen eigenen Mechanismus mit: Ein Formular mit method="dialog" schließt den Dialog ohne Request, dialog.returnValue enthält den value des Buttons.
<dialog id="loeschen-dialog" aria-labelledby="loeschen-titel" aria-describedby="loeschen-frage">
<h2 id="loeschen-titel">Eintrag löschen?</h2>
<p id="loeschen-frage">„Rechnung 2026-041" wird dauerhaft entfernt.</p>
<form method="dialog">
<button value="abbrechen" autofocus>Abbrechen</button>
<button value="loeschen">Endgültig löschen</button>
</form>
</dialog>
const dialog = document.getElementById('loeschen-dialog');
dialog.addEventListener('close', () => {
if (dialog.returnValue === 'loeschen') {
// erst hier wird wirklich gelöscht
}
});
Drei Details tragen das Muster: autofocus liegt nach der APG-Regel auf der am wenigsten destruktiven Aktion, Esc entspricht „Abbrechen“ ohne eigenen Handler, und beide Aktionen sind echte Buttons, keine Links oder Spans.
dialog oder alertdialog?
role="alertdialog" ist für Meldungen reserviert, die unterbrechen und eine Reaktion erzwingen – „Sitzung läuft ab“, „Ungespeicherte Änderungen verwerfen?“. Laut W3C APG ist ein Alertdialog definitionsgemäß modal – modal wird er aber erst durch showModal() bzw. gesetztes aria-modal. Die Rolle setzt du direkt aufs <dialog> – einer der wenigen Fälle für eine explizite Rolle. Alles andere bleibt bei dialog; dringliche Meldungen ohne Dialog zeigen die barrierefreien Fehlermeldungen.
Backdrop, Scroll-Sperre und closedby
Den abgedunkelten Hintergrund rendert der Browser als ::backdrop – gestaltbar ohne zusätzliches Markup:
dialog::backdrop {
background: rgb(27 31 39 / 0.55);
backdrop-filter: blur(2px);
}
/* Scroll-Sperre: showModal() macht den Hintergrund inert,
verhindert aber nicht überall das Mitscrollen des Dokuments */
body:has(dialog:modal) {
overflow: hidden;
}
Die zweite Regel vergessen fast alle: Inert heißt nicht scrollfest. Ohne Sperre scrollt die Seite hinter dem Modal weiter – irritierend für Sehende, desorientierend bei starker Vergrößerung; :has() unterstützen alle aktuellen Browser.
Das closedby-Attribut regelt Light-Dismiss nativ: any schließt per Klick außerhalb und per Esc, closerequest (das Verhalten von showModal()) nur per Esc, none gar nicht. Stand Juli 2026 ist es laut
Web Features Explorer
„Limited availability“: Chrome und Edge 134 (März 2025) sowie Firefox 141 (Juli 2025) unterstützen es, Safari noch nicht – bis dahin bleibt Klick-daneben-Schließen ein kleiner Event-Handler.
Popover sind immer nicht-modal – die Abgrenzung, die ständig durcheinandergeht: Die Popover-API (Baseline seit Januar 2025) sperrt weder Hintergrund noch Fokus. Welches Bauteil wann passt, sortiert Tooltips und die Popover-API.
Der ARIA-Fallback: nur noch ein Wartungsfall
Neu bauen würde ich ein div-Modal nicht mehr – das verstößt gegen die erste Regel von ARIA, in Bestandsprojekten musst du es aber warten: Container mit role="dialog", aria-modal="true" und aria-labelledby, Hintergrund per inert deaktiviert, Fokus-Rückgabe von Hand – und die Tab-Schleife:
// Kern des Eigenbaus: Tab im Dialog halten, Esc schließt
const fokussierbar = eigenbau.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea'
);
const erstes = fokussierbar[0];
const letztes = fokussierbar[fokussierbar.length - 1];
eigenbau.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { schliessen(); return; }
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === erstes) {
e.preventDefault(); letztes.focus();
} else if (!e.shiftKey && document.activeElement === letztes) {
e.preventDefault(); erstes.focus();
}
});
Das Gegenbeispiel aus Audits:
<!-- Gegenbeispiel: optisch ein Modal, technisch ein div -->
<div class="overlay">
<div class="box">
<span class="schliessen" onclick="overlayZu()">×</span>
<p>Möchtest du den Eintrag wirklich löschen?</p>
</div>
</div>
Keine Rolle, kein Name, kein fokussierbares Schließen-Element. Die historischen Gründe für solche Nachbauten sind überholt: 2018 unterstützten nur Chrome und Opera das dialog-Element, Firefox hatte aria-modal-Schwächen, JAWS mit Internet Explorer 11 wertete das Attribut nicht aus (Scott O’Hara, 2019). Wer heute davon abrät, argumentiert mit einem seit vier Jahren erledigten Stand.
Der Preis, den ein Modal in der Messung kostet
Ein Dialog, der beim Laden von selbst aufgeht – Newsletter, Rabattcode, Umfrage –, ist nicht nur eine Barriere, er ist auch messbar teuer: Er verschiebt Inhalt, verzögert die erste Interaktion und wird von Google zusätzlich als aufdringliches Interstitial gewertet. Was davon in welche Kennzahl einfließt, steht unter Core Web Vitals. Für Consent-Dialoge gilt dasselbe in verschärfter Form – dazu Cookie-Banner.
Muss es überhaupt ein Modal sein?
Ein Modal erzwingt eine Unterbrechung – gerechtfertigt bei destruktiven Aktionen und echten Entscheidungen, nicht für Newsletter-Werbung nach drei Sekunden. Reicht eine Inline-Lösung (Meldung im Kontext, aufklappender Bereich, Popover), ist sie fast immer besser.
So testest du es
Im Kern ist es der BITV-Prüfschritt zu 2.1.2, ausdrücklich in Firefox und Chrome mit eingeschaltetem JavaScript, ergänzt um Fokus- und Screenreader-Proben:
- Per Tastatur öffnen: mit Tab zum Auslöser, dann Enter – der Fokus muss sichtbar im Dialog liegen.
- Tab mehrfach drücken: Der Fokus zirkuliert; nichts dahinter ist erreichbar.
- Shift + Tab auf dem ersten Element: Der Fokus springt zum letzten.
- Esc drücken: Der Dialog schließt, der Fokus liegt wieder auf dem Auslöser.
- Screenreader-Gegenprobe mit NVDA oder VoiceOver: Rolle und Titel werden angesagt, der Hintergrund bleibt auch im Lesemodus gesperrt.
- Scroll-Probe: Mausrad über dem Backdrop – scrollt die Seite mit, fehlt die Sperre.
- Auf 200 % zoomen: Der Dialog bleibt sichtbar und bedienbar (Vergrößerbarkeit nach EN 301 549).
Das Vorgehen für ganze Seiten beschreibt Selbst testen, den formalen Rahmen der BITV-Test.
Häufiger Fehler in der Praxis
Das dialog-Element, das per CSS-Klasse „geöffnet“ wird. Statt showModal() togglet das JavaScript eine Klasse oder das open-Attribut: kein Top-Layer, keine Fokusfalle, kein modaler Status. Weil es optisch stimmt, fällt es erst beim Tab-Durchgang auf.
Fokus-Rückgabe an den Seitenanfang. Der Dialog schließt, document.body bekommt den Fokus, und alle tabben sich von vorn durch die Navigation. Häufigste Ursache: Der Dialog wird aus dem DOM entfernt, bevor der Fokus zurückgesetzt ist.
Der ×-Schriftzug als Schließen-Element. Ein <span> mit Klick-Handler ist nicht fokussierbar, hat keine Rolle und keinen Namen – und „ד wird, wenn überhaupt, als „Multiplikationszeichen“ angesagt. Richtig: ein <button type="button"> mit sichtbarem Fokus und aria-label="Schließen".
aria-hidden-Wildwuchs im Fallback. Der Hintergrund bekommt aria-hidden="true" – aber der Dialog steckt in einem Wrapper, der mit versteckt wird, und verschwindet für Screenreader. Mit inert auf den Geschwister-Containern passiert das nicht, weil der Dialog zwingend außerhalb liegt.
Ob natives <dialog> oder ARIA-Nachbau: Beide Varianten stehen im Komponenten-Baukasten zum Umschalten nebeneinander – mit bedienbarer Vorschau, Code und einem Testprotokoll, das den Tastaturweg und die erwartete Screenreader-Ausgabe Schritt für Schritt beschreibt.
Häufige Fragen
Ist das HTML-dialog-Element barrierefrei?
Ja – mit showModal() erfüllt es die Kernanforderungen von allein: Fokus hinein, Fokusfalle, Esc, Fokus-Rückgabe, implizite Rolle und aria-modal="true". Bei dir bleiben der zugängliche Name per aria-labelledby und ein sinnvoller Initialfokus. Die W3C-Technik H102 führt es offiziell als Weg zu barrierefreien modalen Dialogen; die Screenreader-Lücken von 2018 sind geschlossen.
Braucht das dialog-Element noch role=“dialog” und aria-modal=“true”?
Nein. role="dialog" ist die implizite Rolle des Elements, und showModal() exponiert aria-modal="true" von selbst – beides manuell zu setzen ist redundant; die verbreitete Gegenempfehlung stammt aus der Zeit vor breitem Browser-Support. Entscheidend ist die Öffnungsart: Bei show() oder dem open-Attribut gilt das Element nicht als modal und ist kein Modal.
Was ist der Unterschied zwischen show() und showModal()?
showModal() öffnet modal: Top-Layer, ::backdrop, Esc, inerter Hintergrund, aria-modal="true". show() öffnet denselben Dialog nicht-modal, die Seite dahinter bleibt bedienbar. Für erzwungene Entscheidungen ist showModal() richtig, für begleitende Panels eher ein Popover. Wer show() für ein Modal nutzt, baut ein Overlay, hinter dem der Fokus frei weiterläuft.
Was ist der Unterschied zwischen dialog und popover?
Ein Popover ist immer nicht-modal: Es sperrt weder Hintergrund noch Fokus – für modale Overlays verweist MDN auf das dialog-Element. Beide nutzen den Top-Layer; <dialog popover> ist valide und dann nicht-modal mit Light-Dismiss. Merksatz: Muss eine Entscheidung fallen, bevor es weitergeht, ist es showModal() – sonst ein Popover.
Darf ich das Schließen per Esc unterbinden?
Technisch ja: preventDefault() im cancel-Event blockiert Esc. Vertretbar ist das nur ausnahmsweise – etwa wenn ungesicherte Eingaben verloren gingen und stattdessen eine Rückfrage erscheint. Esc ist der Standard-Ausweg, den WCAG 2.1.2 für die erlaubte Fokusbeschränkung im Modal voraussetzt; wer ihn ersatzlos streicht, baut eine Tastaturfalle.
Verwandte Themen
- Tooltips & Popover-API – wann ein Popover statt eines Modals genügt
- Die erste Regel von ARIA – warum das native Element dem Nachbau immer vorgeht
- Tastaturbedienung & sichtbarer Fokus – die Grundlagen hinter Fokusfalle und Fokus-Rückgabe
- Rollen, States & Properties – aria-labelledby, aria-describedby und Co. im Detail
- Cookie-Banner – der häufigste Dialog im Netz mit Sonderregeln
- Mit NVDA testen – die Screenreader-Gegenprobe für jeden Dialog
- The Dialog element – MDN Web Docs
- Dialog (Modal) Pattern – WAI-ARIA Authoring Practices Guide (W3C)
- Understanding SC 2.4.11 Focus Not Obscured (Minimum) – W3C
- Prüfschritt 2.1.2 Keine Tastaturfalle – BIK BITV-Test