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, Ende und Buchstaben-Sprung – alles in Eigenregie.
  • aria-haspopup gehört nicht an ein Disclosure. Der Wert true ist gleichbedeutend mit menu und kündigt genau das Anwendungsmenü an, das du nicht gebaut hast.
  • aria-expanded muss 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 ein aria-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:

  1. Die Einträge sind Befehle, keine Ziele. Ein menuitem löst etwas aus – umbenennen, löschen, exportieren. Ein Link führt woandershin. Screenreader kündigen menuitem entsprechend an, und wer daraufhin einen Befehl erwartet, bekommt einen Seitenwechsel.
  2. Nur ein Tabstop für das ganze Menü. Innerhalb eines menu navigiert man mit Pfeiltasten, nicht mit Tab. Das musst du über Roving Tabindex selbst umsetzen, samt Home, Ende, Buchstaben-Sprung und Umbruch am Ende der Liste.
  3. 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“).

Zwei Gegenüberstellungen desselben Menüs mit den Punkten Produkte, Preise und Hilfe sowie einem offenen Panel mit den Links Analyse und Export. Links, rot markiert als „Anwendungsmenü-Rolle“ mit role=menubar: Tab springt über das ganze Menü hinweg, weil alle Punkte in einer einzigen Tabstop-Gruppe liegen; Pfeil rechts und links wechseln zwischen den Punkten, Pfeil abwärts öffnet das Untermenü und Esc schließt es – alle drei Tastenwege sind rot als „muss selbst programmiert werden“ markiert. Rechts, grün markiert als „Disclosure-Muster“ mit button plus aria-expanded: Tab läuft Punkt für Punkt und in das offene Panel hinein, Enter und Leertaste klappen auf und zu, Tab führt wieder heraus ohne Fokus-Trap – alle drei sind grün als vom Browser geliefert markiert; nur Esc ist orange markiert als die eine Zeile JavaScript, die es braucht.
Vier selbst zu bauende Tastenwege gegen einen: Die Rollenwahl entscheidet, wie viel Tastaturlogik du schreibst.

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 – inert am 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 :hover und :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ührt aria-expanded mit und behandelt Esc.

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-expanded wird 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)
Gratis E-Book PDF Das Praxishandbuch

Kostenloses E-Book

Das Praxishandbuch für sauberes, zugängliches Web

Alles rund um semantisches HTML, Barrierefreiheit, WCAG & BFSG, GEO und SEO — praxisnah und am echten Code. In mehreren Feedbackschleifen von Leserinnen und Lesern verbessert.

  • 3.000+ Downloads
  • 7. Auflage
  • 37 Seiten
  • PDF

Kein Spam. Abmeldung jederzeit mit einem Klick möglich.