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.
Inhaltsverzeichnis
- 1. Warum Accordion und Tabs eigene ARIA-Patterns brauchen
- 2. Das Accordion-Pattern: aria-expanded, aria-controls und Button-Semantik
- 3. Das Tabs-Pattern: role="tablist", role="tab" und role="tabpanel"
- 4. Pfeiltasten-Navigation bei Tabs korrekt implementieren
- 5. Enter und Space bei Accordions: warum native Buttons das automatisch lösen
- 6. Praxisbeispiel: Produktbeschreibung-Tabs auf der Hyvä-Produktseite
- 7. Praxisbeispiel: FAQ-Accordion mit mehreren gleichzeitig offenen Panels
- 8. Test-Checkliste für beide Patterns
- 9. Häufige Abweichungen vom Standard-Pattern und ihre Folgen
- 10. Zusammenfassung
- 11. FAQ
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.