Accordion und Tabs barrierefrei implementieren
AI generated
A11Y
WCAG
Barrierefreiheit
Accordion und Tabs barrierefrei
Korrekte ARIA-Patterns implementieren

Accordion und Tabs zählen zu den am häufigsten falsch umgesetzten Interface-Mustern im Web, weil sie visuell einfach wirken, aber unter der Oberfläche eine präzise definierte Tastatur-Interaktion und eine eigene ARIA-Rollenstruktur erfordern. Ein Accordion, das nur per Klick auf ein div reagiert, oder Tabs, die keine Pfeiltasten-Navigation kennen, funktionieren für Maus-Nutzer scheinbar einwandfrei und bleiben für Tastatur- und Screenreader-Nutzer trotzdem unbenutzbar oder zumindest missverständlich.

10 Min. Lesezeit role="tablist" aria-expanded WAI-ARIA Authoring Practices

1. Warum Accordion und Tabs eigene ARIA-Patterns brauchen

Beide Komponenten erscheinen auf den ersten Blick simpel: Ein Klick zeigt oder versteckt Inhalt. Diese Vereinfachung ignoriert jedoch, dass Screenreader-Nutzer ohne semantische Auszeichnung nicht wissen können, dass ein bestimmtes Element überhaupt aufklappbar ist, welchen Zustand es gerade hat, offen oder geschlossen, aktiv oder inaktiv, und wie viele weitere gleichartige Elemente in der Gruppe existieren. Ein div mit einem Klick-Handler liefert visuell dieselbe Optik wie ein korrekt ausgezeichneter Button, aber semantisch gar nichts.

Die WAI-ARIA Authoring Practices definieren für beide Muster präzise Rollen-, Zustands- und Tastatur-Vorgaben, die in praktisch jedem Screenreader konsistent unterstützt werden, sofern sie exakt eingehalten werden. Abweichungen von diesen Vorgaben, etwa eine eigene Tastatur-Logik statt der erwarteten Pfeiltasten-Navigation bei Tabs, führen regelmäßig zu Verwirrung, weil erfahrene Screenreader-Nutzer sich auf das Standardverhalten verlassen und bei Abweichungen zunächst annehmen, ihre eigene Bedienung sei fehlerhaft.

2. Das Accordion-Pattern: aria-expanded, aria-controls und Button-Semantik

Ein barrierefreies Accordion besteht aus einer Überschrift, meist h3, die einen button umschließt, sowie einem zugehörigen Inhaltsbereich. Der Button trägt aria-expanded mit dem aktuellen Zustand, true für geöffnet und false für geschlossen, sowie aria-controls, das per ID-Referenz auf den zugehörigen Inhaltsbereich verweist. Der Inhaltsbereich selbst braucht keine spezielle ARIA-Rolle, sollte aber über diese ID eindeutig referenzierbar sein.

Entscheidend ist die native Verwendung des button-Elements statt eines div oder span mit Klick-Handler. Ein natives button-Element ist automatisch per Tab erreichbar, reagiert automatisch auf Enter und Space, und trägt bereits die Rolle 'button' im Accessibility Tree, ohne dass diese manuell per role-Attribut nachgetragen werden müsste. Wird dennoch ein Nicht-Button-Element verwendet, müssen role="button", tabindex="0" sowie manuelle Tastatur-Handler für Enter und Space ergänzt werden, was fehleranfälliger ist als die native Lösung.


<!-- Barrierefreies Accordion-Item: natives button-Element -->
<h3>
  <button type="button"
          :aria-expanded="open.toString()"
          aria-controls="panel-versand"
          @click="open = !open"
          class="w-full flex items-center justify-between py-4 text-left">
    <span>Versandkosten</span>
    <svg :class="{ 'rotate-180': open }" class="h-5 w-5 transition-transform" aria-hidden="true">
      <!-- Chevron-Icon -->
    </svg>
  </button>
</h3>
<div id="panel-versand" x-show="open" role="region" aria-labelledby="panel-versand-heading">
  <p>Versandkosten betragen 4,90 Euro innerhalb Deutschlands.</p>
</div>

3. Das Tabs-Pattern: role="tablist", role="tab" und role="tabpanel"

Ein barrierefreies Tab-Widget besteht aus drei zusammenspielenden Rollen. Der umschließende Container der Tab-Buttons erhält role="tablist", jeder einzelne Tab-Button role="tab" zusammen mit aria-selected, das den aktiven Tab markiert, und jeder Inhaltsbereich role="tabpanel". Zusätzlich verweist jeder Tab per aria-controls auf sein zugehöriges Panel, und jedes Panel per aria-labelledby zurück auf seinen Tab.

Ein häufig übersehenes Detail: Nur der aktive Tab trägt tabindex="0", alle inaktiven Tabs erhalten tabindex="-1". Das Tab-Widget ist damit als Ganzes nur ein einziges Tab-Stopp in der globalen Seiten-Tab-Reihenfolge, während die Navigation zwischen den einzelnen Tabs innerhalb des Widgets über Pfeiltasten erfolgt, nicht über wiederholtes Tab-Drücken. Dieses Muster nennt sich Roving Tabindex und ist zentral für die korrekte Tastatur-Erfahrung von Tab-Widgets.


<!-- Tab-Liste mit Roving Tabindex: nur der aktive Tab ist per Tab erreichbar -->
<div role="tablist" aria-label="Produktinformationen">
  <button role="tab"
          id="tab-beschreibung"
          :aria-selected="activeTab === 'beschreibung'"
          :tabindex="activeTab === 'beschreibung' ? 0 : -1"
          aria-controls="panel-beschreibung"
          @click="activeTab = 'beschreibung'">
    Beschreibung
  </button>
  <button role="tab"
          id="tab-technisch"
          :aria-selected="activeTab === 'technisch'"
          :tabindex="activeTab === 'technisch' ? 0 : -1"
          aria-controls="panel-technisch"
          @click="activeTab = 'technisch'">
    Technische Daten
  </button>
</div>
<div id="panel-beschreibung" role="tabpanel" aria-labelledby="tab-beschreibung" x-show="activeTab === 'beschreibung'">
  <!-- Beschreibungstext -->
</div>

4. Pfeiltasten-Navigation bei Tabs korrekt implementieren

Innerhalb eines fokussierten Tab-Widgets erwarten Screenreader-Nutzer, dass die rechte und linke Pfeiltaste, bei vertikal angeordneten Tabs oben und unten, zum nächsten beziehungsweise vorherigen Tab springen und diesen automatisch aktivieren. Zusätzlich sollten Home und End zum ersten beziehungsweise letzten Tab springen. Die Aktivierung erfolgt bei den meisten Implementierungen automatisch beim Fokuswechsel, ohne dass zusätzlich Enter gedrückt werden muss, dieses Verhalten nennt sich 'automatic activation' in den WAI-ARIA Authoring Practices und ist der empfohlene Standard für Tabs, die keine aufwendige Inhaltsberechnung pro Panel-Wechsel auslösen.

In Alpine.js lässt sich diese Navigation kompakt über @keydown.right und @keydown.left auf dem Tablist-Container implementieren, kombiniert mit einer Methode, die den nächsten aktivierbaren Tab-Index berechnet und dessen DOM-Element fokussiert. Wichtig ist, dass bei jedem Pfeiltasten-Wechsel sowohl der Alpine-Zustand activeTab aktualisiert als auch der Browser-Fokus explizit auf den neuen Tab-Button gesetzt wird, da beide sonst auseinanderlaufen können.


// tabs.js: Pfeiltasten-Navigation mit automatischer Aktivierung
function productTabs(tabs) {
  return {
    tabs,
    activeTab: tabs[0].id,
    activateByOffset(offset) {
      const currentIndex = this.tabs.findIndex(t => t.id === this.activeTab);
      const nextIndex = (currentIndex + offset + this.tabs.length) % this.tabs.length;
      this.activeTab = this.tabs[nextIndex].id;
      this.$nextTick(() => {
        document.getElementById(`tab-${this.activeTab}`)?.focus();
      });
    },
    next() { this.activateByOffset(1); },
    prev() { this.activateByOffset(-1); },
  };
}

5. Enter und Space bei Accordions: warum native Buttons das automatisch lösen

Bei Accordions ist die Tastatur-Erwartung einfacher als bei Tabs: Enter und Space sollen den Zustand des fokussierten Accordion-Buttons umschalten, offen wird geschlossen und umgekehrt. Ein natives button-Element löst dieses Verhalten automatisch aus, ohne dass ein eigener keydown-Handler geschrieben werden muss, weil Browser für Buttons standardmäßig sowohl auf Enter als auch auf Space mit einem click-Ereignis reagieren.

Der Grund, warum trotzdem regelmäßig Fehler auftreten: Viele Accordion-Implementierungen binden den Klick-Handler zwar an ein button-Element, verhindern aber versehentlich das Standard-Space-Verhalten, indem sie das Space-Ereignis global abfangen, etwa für eine Such-Shortcut-Funktion, ohne zu prüfen, ob der Fokus gerade auf einem interaktiven Element wie einem Accordion-Button liegt. Solche globalen Tastatur-Shortcuts müssen immer prüfen, ob event.target ein natives interaktives Element ist, bevor sie das Standardverhalten unterdrücken.

6. Praxisbeispiel: Produktbeschreibung-Tabs auf der Hyvä-Produktseite

Auf vielen Magento-Produktseiten werden Beschreibung, technische Daten und Bewertungen als Tabs unterhalb der Produktbilder dargestellt. Eine häufige Fehlimplementierung nutzt reine div-Elemente mit Alpine.js-Klick-Bindungen ohne jegliche ARIA-Rollen, was für sehende Maus-Nutzer völlig unauffällig bleibt, für Screenreader-Nutzer aber wie eine unstrukturierte Ansammlung von Textblöcken wirkt, ohne erkennbaren Zusammenhang zwischen Tab-Label und zugehörigem Inhalt.

Die korrekte Umsetzung kombiniert das Tabs-ARIA-Pattern aus dem vorherigen Abschnitt mit der bestehenden Hyvä-Block-Struktur: Jeder Tab-Inhalt bleibt ein eigenständiger Hyvä-Block, wird aber über ein gemeinsames Alpine-x-data-Objekt auf Ebene des umschließenden Containers gesteuert, damit die Roving-Tabindex-Logik zentral an einer Stelle verwaltet wird, statt in jedem einzelnen Block-Template dupliziert zu werden.


<!-- product/view/tabs.phtml: Hyvä-Blockstruktur mit ARIA-Tabs -->
<div x-data="productTabs([
       { id: 'beschreibung', label: 'Beschreibung' },
       { id: 'technisch', label: 'Technische Daten' },
       { id: 'bewertungen', label: 'Bewertungen' },
     ])">
  <div role="tablist" aria-label="Produktinformationen"
       @keydown.right.prevent="next()" @keydown.left.prevent="prev()"
       @keydown.home.prevent="activeTab = tabs[0].id"
       @keydown.end.prevent="activeTab = tabs[tabs.length - 1].id">
    <template x-for="tab in tabs" :key="tab.id">
      <button role="tab" :id="`tab-${tab.id}`"
              :aria-selected="activeTab === tab.id"
              :tabindex="activeTab === tab.id ? 0 : -1"
              :aria-controls="`panel-${tab.id}`"
              @click="activeTab = tab.id" x-text="tab.label"></button>
    </template>
  </div>
  <div id="panel-beschreibung" role="tabpanel" aria-labelledby="tab-beschreibung"
       x-show="activeTab === 'beschreibung'">
    <?= $block->getChildHtml('product.info.description') ?>
  </div>
</div>

7. Praxisbeispiel: FAQ-Accordion mit mehreren gleichzeitig offenen Panels

FAQ-Bereiche unterscheiden sich von klassischen Accordions oft darin, dass mehrere Panels gleichzeitig geöffnet sein dürfen, statt dass sich das Öffnen eines Panels automatisch auf das Schließen aller anderen auswirkt. Das ARIA-Pattern bleibt dabei identisch, jeder Button trägt weiterhin aria-expanded und aria-controls, es gibt lediglich keine zentrale Logik, die beim Öffnen eines Panels alle anderen automatisch schließt.

In Magento eignet sich für FAQ-Bereiche häufig auch das native HTML-Element details und summary, das ohne jegliches JavaScript und ohne manuelle ARIA-Attribute bereits vollständig tastatur- und screenreaderzugänglich ist, weil der Browser aria-expanded-äquivalentes Verhalten automatisch bereitstellt. Der Nachteil ist eine eingeschränktere visuelle Anpassbarkeit der Aufklapp-Animation im Vergleich zu einer Alpine.js-Lösung, weshalb viele Hyvä-Projekte sich für die manuelle button-Variante entscheiden, wenn eine animierte Höhen-Transition gewünscht ist.


<!-- FAQ-Accordion: mehrere Panels gleichzeitig offen möglich -->
<div x-data="{ openItems: new Set() }">
  <template x-for="item in faqItems" :key="item.id">
    <div>
      <h3>
        <button type="button"
                :aria-expanded="openItems.has(item.id)"
                :aria-controls="`faq-panel-${item.id}`"
                @click="openItems.has(item.id) ? openItems.delete(item.id) : openItems.add(item.id)">
          <span x-text="item.question"></span>
        </button>
      </h3>
      <div :id="`faq-panel-${item.id}`" x-show="openItems.has(item.id)" x-text="item.answer"></div>
    </div>
  </template>
</div>

8. Test-Checkliste für beide Patterns

Für Accordions: Maus weglegen, per Tab jeden Accordion-Button erreichen, mit Enter und Space öffnen und schließen, und prüfen, ob der Screenreader den Zustandswechsel ansagt, was bei korrekt gesetztem aria-expanded automatisch geschieht, ohne zusätzliche Live-Region. Für Tabs: prüfen, dass nur ein einziger Tab-Stopp für das gesamte Widget in der globalen Tab-Reihenfolge existiert, dass Pfeiltasten innerhalb des Widgets zum nächsten Tab springen und diesen aktivieren, und dass Home und End zum ersten beziehungsweise letzten Tab führen.

Ein zusätzlicher, oft übersehener Test betrifft die Panel-Inhalte selbst: Nach dem Wechsel zu einem anderen Tab sollte der Fokus in aller Regel nicht automatisch in das neue Panel springen, sondern auf dem aktivierten Tab-Button verbleiben, damit der Nutzer per Tab bewusst entscheidet, wann er in den Panel-Inhalt hineinnavigiert, statt unerwartet aus dem Tab-Widget herauskatapultiert zu werden.

9. Häufige Abweichungen vom Standard-Pattern und ihre Folgen

Eine häufige Abweichung ist die Verwendung von role="tab" ohne begleitendes role="tablist" auf dem Container, was dazu führt, dass Screenreader die einzelnen Tabs zwar als solche ankündigen, aber keine Gruppierungsinformation liefern können, etwa 'Tab 2 von 4'. Eine zweite häufige Abweichung ist ein Accordion, bei dem aria-expanded zwar technisch vorhanden ist, aber nicht dynamisch aktualisiert wird, weil es als statisches Attribut im Server-Rendering gesetzt und nie per JavaScript mit dem tatsächlichen Sichtbarkeitszustand synchronisiert wurde.

Beide Abweichungen sind mit automatisierten Tools wie axe-core teilweise erkennbar, insbesondere die fehlende tablist-Rolle, während die fehlende dynamische Aktualisierung von aria-expanded oft nur im manuellen Test mit einem echten Screenreader auffällt, weil axe-core den statischen HTML-Zustand zum Zeitpunkt des Tests prüft, nicht das Verhalten nach einer Nutzerinteraktion.

Pattern Wichtigste Rolle/Attribut Tastatur-Interaktion Typischer Magento-Fehler
Accordion aria-expanded auf dem button Enter/Space, nativ über button-Element aria-expanded statisch gesetzt, nie aktualisiert
Tabs (horizontal) role="tablist" auf Container Pfeil links/rechts, Home/End role="tab" ohne umschließendes tablist
Tabs (Roving Tabindex) tabindex 0 nur auf aktivem Tab Nur ein Tab-Stopp im globalen Tab-Flow Alle Tabs mit tabindex=0, mehrere Stopps nötig
FAQ-Accordion (mehrfach offen) aria-controls je Panel Enter/Space pro Panel unabhängig Klick schließt fälschlich alle anderen Panels
Natives details/summary Kein ARIA nötig, Browser-nativ Enter/Space automatisch unterstützt Unnötig mit zusätzlichem ARIA überladen

Mironsoft

WCAG-Audits, barrierefreie Magento-Shops und Schulungen

Unsicher, ob der Shop wirklich barrierefrei ist?

Wir prüfen bestehende Magento-Shops gegen WCAG 2.2, beheben konkrete Barrieren im Hyvä-Frontend und schulen Teams, damit Barrierefreiheit dauerhaft im Entwicklungsprozess verankert bleibt.

WCAG-Audit

Shop systematisch gegen WCAG 2.2 AA prüfen, mit priorisierter Fehlerliste.

Barrieren beheben

Konkrete Umsetzung: Tastaturbedienbarkeit, Screenreader-Support, Kontraste, Formulare.

Team-Schulung

Entwickler und Redakteure für barrierefreie Umsetzung im Alltag sensibilisieren.

10. Zusammenfassung

Accordion und Tabs barrierefrei: Das Wichtigste auf einen Blick

Accordion-Kern

Natives button-Element mit aria-expanded und aria-controls, dynamisch aktualisiert bei jedem Zustandswechsel, nicht statisch im Server-Rendering fixiert.

Tabs-Kern

Dreiteiliges Rollenmuster role=tablist, role=tab, role=tabpanel kombiniert mit Roving Tabindex, sodass nur der aktive Tab per Tab-Taste erreichbar ist.

Tastatur-Standard

Pfeiltasten navigieren und aktivieren Tabs automatisch, Home und End springen an Anfang und Ende, Enter und Space schalten Accordion-Panels um.

Häufigster Fehler

ARIA-Rollen ohne begleitende Tastatur-Logik oder ohne dynamische Attribut-Aktualisierung, wodurch die Auszeichnung technisch vorhanden, aber funktional wirkungslos bleibt.

11. FAQ: Accordion und Tabs barrierefrei: Das Wichtigste auf einen Blick

1Muss ein Accordion-Button zwingend ein natives button-Element sein?
Es ist die deutlich robustere Lösung, weil native buttons automatisch per Tab erreichbar sind und automatisch auf Enter und Space reagieren. Alternativen mit div und role=button erfordern manuell nachgebaute Tastatur-Handler und sind fehleranfälliger.
2Was ist Roving Tabindex und warum brauchen Tabs dieses Muster?
Roving Tabindex bedeutet, dass nur ein einziges Element einer Gruppe zur gleichen Zeit tabindex=0 trägt, alle anderen tabindex=-1. Dadurch ist das gesamte Tab-Widget nur ein einziger Stopp in der globalen Tab-Reihenfolge, während Pfeiltasten innerhalb des Widgets zwischen den Tabs wechseln.
3Müssen Tabs bei jedem Fokuswechsel sofort aktiviert werden?
Das ist die empfohlene 'automatic activation', solange der Panel-Wechsel keine aufwendige Berechnung auslöst. Bei teuren Inhaltsberechnungen ist auch 'manual activation' erlaubt, bei der erst Enter den Tab tatsächlich aktiviert.
4Dürfen bei einem FAQ-Accordion mehrere Panels gleichzeitig offen sein?
Ja, das ist ein legitimes, häufig genutztes Muster und weicht nicht vom ARIA-Pattern ab. Jeder Button trägt weiterhin unabhängig sein eigenes aria-expanded, es fehlt lediglich die Logik, die andere Panels automatisch schließt.
5Reicht das native details/summary-Element als Ersatz für ein selbstgebautes Accordion?
Für viele Fälle ja, es ist vollständig tastatur- und screenreaderzugänglich ohne zusätzliches ARIA. Der Nachteil ist eine eingeschränktere Kontrolle über animierte Höhen-Übergänge im Vergleich zu einer Alpine.js-Lösung.
6Warum meldet axe-core einen Tabs-Fehler, obwohl role=tab korrekt gesetzt ist?
axe-core prüft unter anderem, ob role=tab von einem umschließenden role=tablist begleitet wird. Fehlt diese Container-Rolle, wird der Fehler gemeldet, selbst wenn die einzelnen Tab-Elemente für sich genommen korrekt annotiert sind.
7Muss aria-expanded bei Server-seitigem Rendering dynamisch sein?
Ja, ein statisch im HTML gesetzter Wert, der nie mit dem tatsächlichen Sichtbarkeitszustand synchronisiert wird, täuscht Screenreadern einen falschen Zustand vor, sobald sich der Panel-Zustand per JavaScript ändert.
8Springt der Fokus beim Tab-Wechsel automatisch in den neuen Panel-Inhalt?
In den meisten Implementierungen nicht und das ist auch korrekt so. Der Fokus bleibt auf dem aktivierten Tab-Button, damit der Nutzer per eigener Tab-Taste bewusst entscheidet, wann er in den Panel-Inhalt navigiert.
9Wie teste ich Pfeiltasten-Navigation bei Tabs ohne Screenreader?
Maus weglegen, einen Tab per Tab-Taste fokussieren und dann ausschließlich die Pfeiltasten nutzen, um zwischen den Tabs zu wechseln. Home und End sollten zum ersten beziehungsweise letzten Tab springen.
10Was passiert, wenn ein globaler Tastatur-Shortcut das Space-Verhalten eines Accordion-Buttons stört?
Wird das Space-Ereignis global für eine andere Funktion abgefangen, ohne zu prüfen, ob der Fokus auf einem interaktiven Element liegt, verhindert das fälschlich das Umschalten des Accordion-Zustands. Globale Shortcuts müssen event.target vor der Ausführung prüfen.