Komponenten · Navigation & Bedienelemente
Menüs & Dropdowns barrierefrei umsetzen
Ein barrierefreies Navigationsmenü ist ein <button aria-expanded>, der eine Liste gewöhnlicher Links ein- und ausblendet – das Disclosure-Muster. role="menu" und role="menubar" gehören nicht in eine Seitennavigation: Sie erzwingen ein Anwendungs-Tastaturmodell mit Pfeiltasten, das du komplett selbst programmieren musst und das auf einer Website niemand erwartet.
Ausklappbare Menüs sind der Teil einer Website, an dem Barrierefreiheit am häufigsten kippt – und meist nicht aus Nachlässigkeit, sondern wegen zweier Fehlannahmen: dass ein Navigationsmenü die Rolle menu brauche, und dass aria-haspopup dazugehöre. Beides ist in den allermeisten Fällen falsch und macht die Bedienung schlechter, nicht besser.
Das Wichtigste in Kürze
- Für aufklappbare Navigation ist das Disclosure-Muster richtig: ein
<button aria-expanded>plus ein Panel mit normalen Links in einer Liste. role="menu"/role="menubar"sind für Anwendungsmenüs gedacht (Datei, Bearbeiten). Sie nehmen die Menüpunkte aus der Tab-Reihenfolge und verlangen Pfeiltasten-Navigation,Home,Endeund Buchstaben-Sprung – alles in Eigenregie.-
aria-haspopupgehört nicht an ein Disclosure. Der Werttrueist gleichbedeutend mitmenuund kündigt genau das Anwendungsmenü an, das du nicht gebaut hast. aria-expandedmuss den sichtbaren Zustand widerspiegeln – jederzeit. Eine falsche Ansage ist schlimmer als keine.- Esc schließt und gibt den Fokus an den auslösenden Button zurück. Das ist die eine Stelle, an der JavaScript unverzichtbar ist.
- Nur-Hover-Menüs fallen durch. Was sich ausschließlich mit der Maus öffnet, ist per Touch und Tastatur unerreichbar; für Aufklappen bei Hover gilt zusätzlich WCAG 1.4.13.
<details>/<summary>ist ein fertiges natives Disclosure – ohne Skript und ohne ARIA. Für einfache Aufklapp-Menüs reicht das oft schon.- Mehrere
<nav>-Bereiche brauchen je einaria-label, sonst sind sie in der Landmark-Liste nicht unterscheidbar.
Warum role="menu" die falsche Abkürzung ist
Die Rolle menu beschreibt ein Bedienelement aus der Welt der Desktop-Anwendungen. Wer sie setzt, gibt drei Versprechen ab, die eine Website meist nicht halten kann:
- Die Einträge sind Befehle, keine Ziele. Ein
menuitemlöst etwas aus – umbenennen, löschen, exportieren. Ein Link führt woandershin. Screenreader kündigenmenuitementsprechend an, und wer daraufhin einen Befehl erwartet, bekommt einen Seitenwechsel. - Nur ein Tabstop für das ganze Menü. Innerhalb eines
menunavigiert man mit Pfeiltasten, nicht mit Tab. Das musst du über Roving Tabindex selbst umsetzen, samtHome,Ende, Buchstaben-Sprung und Umbruch am Ende der Liste. - Der Screenreader wechselt den Modus. Viele Programme schalten in
menu-Strukturen in einen Anwendungsmodus, in dem ihre eigenen Navigationsbefehle nicht mehr greifen – Überschriftensprung, Linkliste, Elementliste sind weg.
Die W3C-Dokumentation zur menu-Rolle sagt es direkt: nicht für Seitennavigation verwenden. Der ARIA Authoring Practices Guide empfiehlt für aufklappbare Navigation das Disclosure-Muster – und liefert dafür ein eigenes Beispiel („Disclosure Navigation Menu“).
Das Disclosure-Muster
Ein Disclosure ist nichts weiter als ein Button, der einen Bereich ein- und ausblendet. Der Button trägt aria-expanded (Zustand) und optional aria-controls (Verweis auf das Panel); im Panel stehen ganz normale Links in einer Liste:
<nav aria-label="Hauptnavigation">
<ul>
<li>
<button type="button" aria-expanded="false" aria-controls="menu-produkte">
Produkte
</button>
<ul id="menu-produkte" hidden>
<li><a href="/analyse.html">Analyse</a></li>
<li><a href="/export.html" aria-current="page">Export</a></li>
</ul>
</li>
</ul>
</nav>
Das ist exakt das Muster, das auch das Mega-Menü dieser Website nutzt: Jeder Themenbereich hat einen <button aria-expanded>, der ein Panel mit normalen Links öffnet. Kein menubar, keine erzwungene Pfeiltasten-Navigation – Buttons und Links, die jeder kennt.
Zu aria-controls eine Einordnung, die man selten liest: Das Attribut ist korrekt, aber seine Wirkung ist begrenzt. Nur wenige Screenreader werten es überhaupt aus – JAWS bietet darüber einen Sprung zum gesteuerten Element an, andere ignorieren es. Es schadet nicht und dokumentiert den Zusammenhang, aber es ersetzt keine sinnvolle DOM-Reihenfolge. Steht das Panel direkt hinter dem Button, findet Tab es ohnehin.
aria-haspopup: das Attribut, das hier nicht hingehört
In Anleitungen taucht regelmäßig die Empfehlung auf, ein Dropdown brauche aria-haspopup="true". Das ist eine der beständigsten Fehlinformationen zum Thema.
aria-haspopup="true" ist laut Spezifikation gleichbedeutend mit aria-haspopup="menu". Es kündigt an: Hier öffnet sich ein Anwendungsmenü mit dem entsprechenden Tastaturmodell. Wenn dann ein Panel mit Links erscheint, in dem Pfeiltasten nichts tun, ist die Ansage falsch. Für ein Disclosure, das einen Bereich zeigt oder verbirgt, genügt aria-expanded – und mehr wäre eine Behauptung über eine Funktion, die es nicht gibt.
Sinnvoll ist aria-haspopup nur mit dem passenden Wert und der passenden Umsetzung: menu bei einem echten Befehlsmenü, dialog bei einem Button, der einen modalen Dialog öffnet, listbox bei einer Combobox.
Das erwartete Tastaturverhalten
Beim Disclosure gilt das vertraute Modell, und genau das ist der Vorteil:
- Enter / Leertaste auf dem Button klappt auf und zu. Kommt vom
<button>, kostet nichts. - Tab läuft logisch weiter – in das geöffnete Panel hinein und wieder heraus. Kein Fokus-Trap, kein Ausbruch nötig.
- Esc schließt das Menü und gibt den Fokus an den auslösenden Button zurück.
- Klick daneben schließt ebenfalls. Reine Höflichkeit, aber erwartetes Verhalten.
const knopf = document.querySelector('[aria-controls="menu-produkte"]');
const panel = document.getElementById('menu-produkte');
function umschalten(offen) {
knopf.setAttribute('aria-expanded', String(offen));
panel.hidden = !offen;
}
knopf.addEventListener('click', () => {
umschalten(knopf.getAttribute('aria-expanded') !== 'true');
});
// Esc schließt – und der Fokus geht dorthin zurück, wo er hergekommen ist.
panel.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
umschalten(false);
knopf.focus();
}
});
knopf.addEventListener('keydown', (e) => {
if (e.key === 'Escape') umschalten(false);
});
Die Synchronisierung von aria-expanded mit dem sichtbaren Zustand ist der eigentliche Kern – und der Teil, den eine reine CSS-Lösung nicht leisten kann. Was Tab, Esc und sichtbarer Fokus grundsätzlich leisten müssen, steht unter Tastaturbedienung & sichtbarer Fokus.
Hover ist Zugabe, nicht Bedienweg
Das Öffnen bei Mausberührung ist bequem und darf bleiben – aber nie als einziger Weg. Zwei Anforderungen kommen dazu, sobald ein Menü per Hover aufgeht:
Es muss auch per Fokus aufgehen. :focus-within am Listenelement erledigt das ohne JavaScript und ist der Grund, warum ein Menü selbst bei abgeschaltetem Skript benutzbar bleibt:
.nav-punkt:hover > .panel,
.nav-punkt:focus-within > .panel {
display: block;
}
Es muss den Regeln für Inhalt bei Hover folgen. 1.4.13 verlangt drei Eigenschaften: Der eingeblendete Inhalt muss ausblendbar sein (Esc), mit dem Zeiger erreichbar sein, ohne zu verschwinden – der Weg von Button zu Panel darf keine Lücke haben, in der es zuklappt –, und bestehen bleiben, bis der Zeiger weg ist oder der Nutzer ihn schließt. Ein Menü, das nach 300 Millisekunden von selbst verschwindet, verstößt dagegen.
Der zweite Punkt ist der, den ich am häufigsten repariere: Zwischen Button und Panel liegt ein 4-Pixel-Spalt, den die Maus überqueren muss. Bei ruhiger Hand fällt das nicht auf, mit Tremor oder Kopfmaus ist das Menü unbenutzbar. Ein padding-block-start am Panel statt margin löst es.
Auf Touch-Geräten gibt es kein Hover. Dort wird der erste Tipp auf den Button zum Öffnen verwendet – und wenn der Button gleichzeitig ein Link ist, springt die Seite weg, bevor das Menü zu sehen war. Deshalb: Auslöser und Ziel trennen. Entweder der Elternpunkt ist ein reiner Button, oder er ist ein Link mit einem separaten kleinen Button daneben. Der braucht dann 24 × 24 Pixel Zielfläche, besser 44.
Das Hamburger-Menü
Mobil ist die Hauptnavigation meist hinter einem Symbol-Button versteckt. Technisch ist das dasselbe Disclosure, mit drei Zusätzen:
<button type="button" aria-expanded="false" aria-controls="hauptmenue">
<span aria-hidden="true">☰</span>
<span class="visually-hidden">Menü</span>
</button>
<nav id="hauptmenue" aria-label="Hauptnavigation" hidden>…</nav>
- Der Button braucht einen Namen. Drei Striche sind kein Text; ein beschrifteter Icon-Button ist Pflicht.
- Bei einem Vollbild-Overlay wird der Rest inert. Wenn das Menü die Seite überdeckt, darf Tab nicht dahinter weiterlaufen –
inertam Hauptinhalt, und der Fokus beginnt im Menü. Das ist dann faktisch ein Dialog und folgt den Regeln von Dialogen und Modals. - Der Zustand wandert mit. Öffnet das Menü, steht
aria-expanded="true"– und die Beschriftung darf mitwechseln („Menü schließen“).
Wann doch role="menu"?
Es gibt einen legitimen Fall: ein echtes Aktionsmenü – etwa ein „⋯ Weitere Aktionen“-Button, dessen Einträge Befehle auslösen (Umbenennen, Löschen, Teilen). So etwas verhält sich wie ein Anwendungsmenü, und dort sind role="menu" mit role="menuitem", aria-haspopup="menu" und Pfeiltasten-Navigation tatsächlich richtig.
Meine Abgrenzung ist simpel: Sind es Links zu Seiten? Dann Disclosure mit <a>. Sind es Befehle, die etwas tun? Dann ist ein Menü mit menuitem denkbar – aber deutlich aufwendiger korrekt umzusetzen, weshalb ich es nur einsetze, wenn es wirklich passt. In gemischten Fällen („Bearbeiten, Duplizieren, Zur Übersicht“) würd ich beim Disclosure mit Buttons und Links darin bleiben: Das Muster verspricht dann weniger und hält mehr.
Progressive Enhancement: erst ohne JS, dann mit
Ich baue solche Menüs so, dass sie ohne JavaScript grundsätzlich funktionieren und mit JavaScript besser werden. Zwei Wege dorthin:
- Reines HTML:
<details>/<summary>ist ein fertiges, natives Disclosure – ohne Skript, ohne ARIA, mit eingebautem Zustand. Für einfache Aufklapp-Menüs oft schon genug. Die Grenzen: Das Schließen bei Esc und bei Klick daneben liefert der Browser nicht, und mehrere gleichzeitig offene Panels muss man selbst verhindern. Details unter details & summary. - CSS-Aufwertung: Panels per
:hoverund:focus-withinöffnen, damit Maus, Touch und Tastatur das Menü auch ohne JS erreichen. Genau dieser Stand ist auf dieser Website umgesetzt; die JS-Schicht darüber führtaria-expandedmit und behandeltEsc.
Wichtig ist mir dabei eine Regel: niemals nur Hover.
Warum die Menüform auch für Crawler zählt
Ein Untermenü, das erst durch JavaScript in den DOM geschrieben wird, existiert für Suchmaschinen im ungünstigen Fall gar nicht – die verlinkten Seiten bleiben dann ohne internen Link und ohne Pfad aus der Navigation. Die robuste Variante ist dieselbe, die auch ohne JavaScript bedienbar ist: echte <a href="…">-Elemente, die im Markup stehen und nur ein- und ausgeblendet werden. Was interne Links dabei tatsächlich bewirken, steht unter Interne Verlinkung als Strategie.
Häufige Fehler
-
role="menubar"für die Seitennavigation. Erzwingt ein Tastaturmodell, das hier niemand erwartet. -
aria-haspopup="true"am Disclosure. Kündigt ein Anwendungsmenü an, das nicht existiert. - Nur per Hover bedienbar. Sperrt Touch und Tastatur aus.
-
aria-expandedwird nicht aktualisiert. Der Button sagt „zu“, obwohl das Menü offen ist. -
<div>als Auslöser. Ohne<button>fehlen Fokus und Tastaturbedienung. - Fokus-Trap im Panel. Tab muss auch wieder herauskommen.
- Lücke zwischen Button und Panel. Das Menü klappt zu, während die Maus unterwegs ist – Verstoß gegen 1.4.13.
- Elternpunkt ist Link und Auslöser gleichzeitig. Auf Touch-Geräten unbedienbar.
Disclosure oder echtes role="menu": Der Komponenten-Baukasten stellt beide Varianten mit Vorschau, Code und Testprotokoll gegenüber – inklusive dem, was die ARIA-Variante kostet.
Häufige Fragen
Disclosure und Akkordeon – ist das nicht dasselbe?
Im Kern ja: Beide bauen auf einem <button aria-expanded>, der einen Bereich zeigt oder verbirgt. Akkordeons sind die Anwendung dieses Musters auf Inhaltsabschnitte, Dropdown-Menüs die Anwendung auf Navigation. Der Unterschied liegt im Inhalt, nicht in der Technik.
Wie kennzeichne ich die aktuelle Seite im Menü?
Mit aria-current="page" am Link der aktiven Seite. So weiß auch ein Screenreader, wo man sich befindet – Farbe oder Fettschrift allein reichen nicht. Diese Website macht das im Mega-Menü und in den Breadcrumbs genauso.
Brauche ich für mehrere <nav>-Bereiche Labels?
Ja. Bei mehreren Navigationen sorgt aria-label für Unterscheidbarkeit – „Hauptnavigation“, „Sie sind hier“, „Fußbereich“. Ohne Namen erscheinen sie in der Landmark-Liste als drei gleichnamige Einträge. Mehr dazu unter Struktur-Elemente.
Sollen Pfeiltasten im Disclosure funktionieren?
Sie müssen nicht, und ich baue es nicht ein. Wer Pfeiltasten ergänzt, muss auch das erwartete Rundum-Verhalten liefern, sonst entsteht ein halbes Modell – und ein halbes Tastaturmodell ist schlechter als ein vertrautes. Tab genügt für Links vollständig.
Muss das Untermenü schließen, wenn ein anderes öffnet?
Bei einer waagerechten Hauptnavigation ja, sonst überdecken sich Panels. Wichtig dabei: Wird ein Panel programmatisch geschlossen, muss sein aria-expanded mitwandern – ein zurückgesetztes Panel mit stehengebliebenem true ist ein Klassiker.
Fazit
Für aufklappbare Navigation ist das Disclosure-Muster die richtige Wahl: ein <button aria-expanded>, der ein Panel mit normalen Links steuert – tastaturbedienbar mit dem vertrauten Tab-Modell, ohne erzwungene menubar-Mechanik und ohne aria-haspopup.
role="menu" hebst du dir für echte Befehlsmenüs auf. Hover darf öffnen, muss aber lückenlos und per Fokus ebenso funktionieren. Und die eine Zeile JavaScript, die es wirklich braucht, ist die für Esc. So bleibt das Menü für alle bedienbar, und du baust kein Tastaturmodell nach, das niemand erwartet.
Quellen
- Disclosure Navigation Menu (W3C ARIA APG – Referenzbeispiel mit Tastaturmodell für aufklappbare Navigation)
- ARIA: menu role (MDN – ausdrücklicher Hinweis, die Rolle nicht für Seitennavigation zu verwenden)