Kein natives Element? Dann volle Verantwortung für role, State und Tastatur
Wer ein eigenes Dropdown oder eine Combobox baut statt ein natives HTML-Element zu nutzen, übernimmt die volle Verantwortung für Tastaturbedienung, Fokus und Screenreader-Ankündigung. Dieser Artikel zeigt Schritt für Schritt, wie ein barrierefreies Custom Widget nach den WAI-ARIA Authoring Practices entsteht, mit korrektem role, aria-expanded, aria-activedescendant und echten Tests mit einem Screenreader vor dem Release.
Inhaltsverzeichnis
- 1. Warum Custom Widgets besondere Sorgfalt brauchen
- 2. Anatomie eines barrierefreien Comboboxes nach WAI-ARIA
- 3. Tastaturbedienung: Pfeiltasten, Escape, Enter und Typeahead
- 4. aria-expanded, aria-activedescendant und aria-selected richtig einsetzen
- 5. Fokus-Management: Roving Tabindex vs. aria-activedescendant
- 6. Implementierung mit Alpine.js im Hyvä-Theme
- 7. Testen mit echten Screenreadern: NVDA, VoiceOver, JAWS
- 8. Häufige Fehler bei Custom Widgets
- 9. Native Elemente vs. Custom Widgets im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Custom Widgets besondere Sorgfalt brauchen
Native HTML-Elemente wie <select>, <input type="checkbox"> oder <button> bringen Barrierefreiheit kostenlos mit. Der Browser kennt ihre Rolle, ihren Zustand und ihre Tastaturbedienung, und jeder Screenreader hat diese Semantik seit Jahrzehnten korrekt implementiert. Sobald ein Frontend-Team ein natives Element durch ein eigenes, frei gestaltbares Dropdown oder eine Autocomplete-Combobox ersetzt, weil sich <select> im Design nicht ausreichend anpassen lässt, verschwindet diese eingebaute Unterstützung vollständig. Ab diesem Moment trägt das eigene Team die volle Verantwortung für Rolle, Zustand, Tastaturlogik und Ankündigung gegenüber assistiven Technologien.
In der Praxis entsteht daraus häufig ein klickbares <div>, das visuell wie ein Dropdown aussieht, aber für Screenreader-Nutzer, Tastaturnutzer und Nutzer von Schaltgeräten (Switch-Devices) faktisch unsichtbar bleibt. Dieses Muster wird umgangssprachlich als „Divitis" bezeichnet: Elemente ohne semantische Rolle, die nur über Mausklicks bedienbar sind. Der zuverlässigste Ausweg ist, keine eigene Interaktionslogik zu erfinden, sondern die etablierten Muster der WAI-ARIA Authoring Practices Guide (APG) zu übernehmen, die von der W3C-Arbeitsgruppe entwickelt und über Jahre in echten Browsern und Screenreadern getestet wurden.
2. Anatomie eines barrierefreien Comboboxes nach WAI-ARIA
Die APG definiert für gängige UI-Muster wie Combobox, Menu, Tabs oder Dialog exakt, welche ARIA-Rollen, -Zustände und -Eigenschaften notwendig sind und in welcher Reihenfolge Tastendrücke welche Effekte auslösen. Für ein Dropdown mit Freitexteingabe ist das Combobox-Pattern mit „list"-Autocomplete-Verhalten maßgeblich: Ein <input> mit role="combobox" steuert eine separate role="listbox" mit role="option"-Kindelementen. Diese Trennung zwischen Eingabefeld und Optionsliste spiegelt exakt, was Screenreader-Nutzer erwarten, wenn sie mit dem nativen <select>- oder <datalist>-Verhalten vertraut sind.
Wichtig ist, dass alle drei Bausteine, also Rolle, Zustand und Eigenschaft, korrekt zusammenspielen: role="combobox" beschreibt die Art des Widgets, aria-expanded beschreibt, ob die Liste sichtbar ist, und aria-controls verweist auf die ID der Listbox. Fehlt eines dieser Attribute oder ist es fehlerhaft gesetzt, kündigt der Screenreader entweder gar nichts oder einen falschen Zustand an. Das folgende Markup zeigt die minimale, korrekte Struktur nach dem APG-Combobox-Pattern.
<!-- WAI-ARIA APG: Editable combobox with list autocomplete -->
<div class="combobox-wrapper relative">
<label id="country-label" for="country-input">Land</label>
<input
id="country-input"
type="text"
role="combobox"
aria-expanded="false"
aria-controls="country-listbox"
aria-autocomplete="list"
aria-activedescendant=""
aria-labelledby="country-label"
autocomplete="off"
>
<ul
id="country-listbox"
role="listbox"
aria-label="Länder"
class="hidden absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-lg"
>
<li id="option-de" role="option" aria-selected="false">Deutschland</li>
<li id="option-at" role="option" aria-selected="false">Österreich</li>
<li id="option-ch" role="option" aria-selected="false">Schweiz</li>
</ul>
</div>
3. Tastaturbedienung: Pfeiltasten, Escape, Enter und Typeahead
Tastaturbedienung ist bei Custom Widgets nicht optional, sie ist die eigentliche Definition von Barrierefreiheit für alle Nutzer, die keine Maus verwenden können oder wollen. Für die Combobox verlangt die APG mindestens: Pfeil-runter öffnet die Liste und bewegt die aktive Option nach unten, Pfeil-hoch bewegt sie nach oben, Enter übernimmt die aktive Option, und Escape schließt die Liste ohne Auswahl und gibt den Fokus zurück an das Eingabefeld. Home und End springen zur ersten beziehungsweise letzten Option, was besonders bei langen Länder- oder Produktlisten den Bedienkomfort deutlich erhöht.
Ein oft übersehenes Detail ist Typeahead: Wer schnell hintereinander Buchstaben tippt, erwartet, dass die Liste zur passenden Option springt, genau wie beim nativen <select>. Wird dieses Verhalten weggelassen, fühlt sich das Custom Widget für erfahrene Tastaturnutzer spürbar langsamer an als das native Original, selbst wenn optisch alles stimmt. Die Implementierung gehört komplett in einen einzigen keydown-Handler, der über eine switch-Anweisung jede relevante Taste behandelt und alle anderen Tasten unangetastet lässt.
// Keyboard handling per WAI-ARIA Authoring Practices combobox pattern
input.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
openListbox();
moveActiveOption(1);
break;
case 'ArrowUp':
event.preventDefault();
moveActiveOption(-1);
break;
case 'Enter':
if (activeOptionId) {
selectOption(activeOptionId);
event.preventDefault();
}
break;
case 'Escape':
closeListbox();
input.focus();
break;
case 'Home':
event.preventDefault();
setActiveOption(options[0].id);
break;
case 'End':
event.preventDefault();
setActiveOption(options[options.length - 1].id);
break;
default:
// Typeahead: jump to the option starting with the typed character
if (event.key.length === 1) {
jumpToTypeahead(event.key);
}
}
});
4. aria-expanded, aria-activedescendant und aria-selected richtig einsetzen
aria-expanded muss synchron mit dem tatsächlich sichtbaren Zustand der Liste sein, niemals nur mit der Absicht. Wird die Liste per CSS mit display: none versteckt, aber aria-expanded bleibt auf „true" stehen, kündigt der Screenreader eine geöffnete Liste an, die de facto nicht existiert, was zu völliger Verwirrung führt. Die Aktualisierung von aria-expanded gehört deshalb in exakt dieselbe Funktion, die auch die CSS-Klasse zum Ein- und Ausblenden setzt, niemals in zwei getrennte Codepfade.
aria-activedescendant ist das zentrale, aber am häufigsten falsch verstandene Attribut des Musters. Der eigentliche DOM-Fokus bleibt die ganze Zeit im <input>-Feld, während aria-activedescendant per ID auf das aktuell hervorgehobene <li role="option"> zeigt. Der Screenreader liest dann so vor, als wäre der Fokus tatsächlich auf der Option, obwohl die Tastatureingabe weiterhin vom Eingabefeld verarbeitet wird. aria-selected markiert schließlich die tatsächlich ausgewählte Option nach einer Bestätigung mit Enter und ist von der nur vorübergehend „aktiven" Option über aria-activedescendant zu unterscheiden. Die visuelle Hervorhebung der aktiven Option muss zwingend per CSS-Klasse nachgebildet werden, weil der Browser dafür keinen nativen Fokusring zeichnet.
/* Visual highlight must mirror aria-activedescendant,
the browser draws no native focus ring for this */
[role="option"][aria-selected="true"] {
background-color: #e4e4e7;
color: #18181b;
font-weight: 600;
}
.combobox-option--active {
background-color: #f4f4f5;
outline: 2px solid #3f3f46;
outline-offset: -2px;
}
/* Never remove the visible focus indicator on the real input */
#country-input:focus-visible {
outline: 2px solid #71717a;
outline-offset: 2px;
}
5. Fokus-Management: Roving Tabindex vs. aria-activedescendant
Für die Verwaltung des Tastaturfokus in zusammengesetzten Widgets gibt es zwei anerkannte Strategien: Roving Tabindex und aria-activedescendant. Bei Roving Tabindex bekommt jeweils nur ein Element in der Gruppe tabindex="0", alle anderen tabindex="-1", und der DOM-Fokus springt bei jeder Pfeiltaste tatsächlich zum nächsten Element. Bei aria-activedescendant, wie im vorigen Abschnitt beschrieben, bleibt der DOM-Fokus konstant auf einem Container-Element, während nur eine ARIA-Referenz den aktiven Nachfolger markiert.
Für eine editierbare Combobox mit Texteingabe ist aria-activedescendant fast immer die richtige Wahl, weil der Fokus zwingend im <input> bleiben muss, damit Tippen weiterhin funktioniert. Roving Tabindex eignet sich besser für reine Auswahlgruppen ohne Texteingabe, etwa eine Toolbar oder ein radiogroup-artiges Menü. Beide Strategien sollten niemals gemischt werden: Ein Widget, das teils den DOM-Fokus verschiebt und teils aria-activedescendant nutzt, erzeugt bei Screenreadern inkonsistente und unvorhersehbare Ankündigungen, weil die Software nicht weiß, welchem Signal sie vertrauen soll.
6. Implementierung mit Alpine.js im Hyvä-Theme
In einem Hyvä-Theme lässt sich das komplette Combobox-Pattern als eigenständige Alpine.js-Komponente kapseln, ohne jQuery oder zusätzliche JavaScript-Bibliotheken zu laden. Der Zustand, also offen oder geschlossen, aktive Option und gefilterte Liste, lebt vollständig in x-data, während ARIA-Attribute über x-bind (kurz :attribut) dynamisch an den tatsächlichen State gebunden werden. Dadurch kann aria-expanded niemals aus dem Tritt geraten, weil es direkt aus derselben reaktiven Variable berechnet wird, die auch die CSS-Klasse für x-show steuert.
Wichtig für Hyvä-Projekte: Der komplette keydown-Handler, das Typeahead-Verhalten und die ID-Generierung für aria-activedescendant gehören in eine wiederverwendbare Alpine-Komponente, die per alpine:init global registriert wird, statt sie in jedem .phtml-Template neu zu duplizieren. So bleibt das Muster für alle Custom-Dropdowns im Shop konsistent, vom Länderauswahlfeld im Checkout bis zur Produktfilterung in der Kategorie-Navigation.
<!-- Hyva phtml: accessible combobox as Alpine.js component -->
<div
x-data="accessibleCombobox({ options: <?= /* @noEscape */ $block->getCountryOptionsJson() ?> })"
class="relative"
>
<label :id="labelId" x-text="label" class="block text-sm font-medium mb-1"></label>
<input
type="text"
role="combobox"
:aria-expanded="open ? 'true' : 'false'"
:aria-activedescendant="activeId"
aria-autocomplete="list"
:aria-controls="listboxId"
:aria-labelledby="labelId"
x-model="query"
x-on:keydown="handleKeydown($event)"
x-on:focus="open = true"
x-on:click.outside="open = false"
class="border border-gray-300 rounded-lg px-3 py-2 w-full"
>
<ul
x-show="open"
:id="listboxId"
role="listbox"
class="absolute z-10 bg-white border border-gray-200 rounded-lg mt-1 w-full"
>
<template x-for="option in filteredOptions" :key="option.id">
<li
:id="option.id"
role="option"
:aria-selected="option.id === activeId"
x-text="option.label"
x-on:click="selectOption(option)"
x-on:mouseenter="activeId = option.id"
class="px-3 py-2 cursor-pointer"
:class="{ 'bg-zinc-100': option.id === activeId }"
></li>
</template>
</ul>
</div>
7. Testen mit echten Screenreadern: NVDA, VoiceOver, JAWS
Kein automatisiertes Tool kann ein Custom Widget vollständig auf Barrierefreiheit prüfen, weil Tools wie axe-core oder Lighthouse nur statische ARIA-Regelverstöße erkennen, etwa fehlende Labels oder ungültige Attributwerte. Ob ein Screenreader die aktive Option beim Drücken der Pfeiltasten tatsächlich verständlich ankündigt, ob die Reihenfolge der Ansagen sinnvoll ist und ob Escape den Fokus korrekt zurücksetzt, lässt sich nur durch manuelles Testen mit einem echten Screenreader herausfinden. Automatisierte Checks sind eine sinnvolle erste Verteidigungslinie in der CI-Pipeline, aber kein Ersatz für den manuellen Test.
Für den produktiven Einsatz empfiehlt sich mindestens eine Kombination aus NVDA mit Firefox unter Windows (kostenlos, weit verbreitet), VoiceOver mit Safari unter macOS oder iOS (in jedes Apple-Betriebssystem vorinstalliert) und idealerweise JAWS mit Chrome, da JAWS in vielen Unternehmensumgebungen und Behörden noch der Standard ist. Ein realistischer Testablauf: Tab zum Eingabefeld, Pfeiltasten zum Navigieren durch die Optionen, Enter zur Auswahl, Escape zum Abbrechen, und dabei genau zuhören, ob der Screenreader Position, Name und Zustand jeder Option korrekt vorliest.
{
"testRunner": "playwright-axe",
"target": "https://shop.example.com/checkout/country-combobox",
"automatedRules": {
"aria-required-attr": "error",
"aria-valid-attr-value": "error",
"aria-command-name": "error",
"listitem": "error"
},
"manualScreenReaderChecks": [
"NVDA + Firefox: option announced with position, e.g. Deutschland, 1 of 3",
"VoiceOver + Safari: aria-activedescendant moves the virtual cursor correctly",
"JAWS + Chrome: Escape closes the listbox and returns focus to the input",
"Keyboard only, no mouse used: Tab order stays predictable throughout"
]
}
8. Häufige Fehler bei Custom Widgets
Der häufigste Fehler ist ein Eingabefeld ohne sichtbares <label> oder ohne aria-labelledby, bei dem nur ein Platzhaltertext als Beschriftung dient. Placeholder-Text verschwindet, sobald der Nutzer zu tippen beginnt, und wird von vielen Screenreader- und Autofill-Kombinationen ohnehin nicht als vollwertiges Label vorgelesen. Ein zweiter häufiger Fehler: aria-expanded wird beim Öffnen gesetzt, aber beim Schließen per Klick außerhalb des Widgets vergessen, sodass der Zustand dauerhaft „true" anzeigt, obwohl die Liste längst unsichtbar ist.
Ein dritter, besonders tückischer Fehler betrifft doppelte Ankündigungen: Wird sowohl ein natives title-Tooltip als auch ein separates aria-label mit unterschiedlichem Text gesetzt, liest mancher Screenreader beide Texte hintereinander vor, was verwirrend klingt. Und schließlich: Wer role="listbox" auf ein Element setzt, aber die Kindelemente nicht mit role="option" versieht, sondern einfache <div>- oder <span>-Elemente ohne Rolle verwendet, erzeugt eine Struktur, die für den Accessibility Tree unvollständig ist und in vielen Screenreadern gar keine Optionen ankündigt.
9. Native Elemente vs. Custom Widgets im Vergleich
Die Entscheidung zwischen nativem Element und Custom Widget mit ARIA sollte nie eine reine Design-Entscheidung sein, sondern immer den zusätzlichen Implementierungs- und Testaufwand mit einbeziehen. Die folgende Übersicht zeigt die kritischen Punkte, an denen selbstgebaute Widgets am häufigsten scheitern, und die jeweils korrekte Umsetzung nach dem APG-Muster.
| Anforderung | Riskant, ohne APG selbst gebaut | Empfehlung nach WAI-ARIA APG | Warum |
|---|---|---|---|
| Dropdown-Auswahl | <div onclick> ohne Rolle |
role="combobox" + role="listbox" |
Screenreader erkennt Widget-Typ und Zustand |
| Tastatur | Nur Mausklick funktioniert | Pfeiltasten, Enter, Escape, Typeahead | Tastaturnutzer sonst vollständig ausgeschlossen |
| Sichtbarkeitsstatus | Kein aria-expanded |
aria-expanded synchron zum Zustand |
Screenreader kündigt sonst falschen Zustand an |
| Aktive Option | Nur visuell hervorgehoben | aria-activedescendant zeigt sie programmatisch |
Fokus bleibt im Input, Ansage bleibt trotzdem korrekt |
| Beschriftung | Nur Platzhaltertext als Label | <label> oder aria-labelledby |
Placeholder verschwindet und wird oft ignoriert |
In der Praxis lohnt sich die Frage vor jedem neuen Custom Widget: Lässt sich das gewünschte Design mit einem gestylten nativen <select> oder mit <input list="..."> (datalist) erreichen? Nur wenn die Antwort eindeutig nein ist, etwa weil Icons, Gruppierungen oder Live-Filterung innerhalb der Optionen benötigt werden, rechtfertigt sich der Mehraufwand eines vollständigen ARIA-Comboboxes.
Mironsoft
Barrierefreie Frontend-Entwicklung für Magento- und Hyvä-Shops
Custom Widgets, die wirklich barrierefrei sind?
Wir entwickeln und auditieren barrierefreie Custom Widgets für Magento- und Hyvä-Shops, von der Combobox über Tabs bis zum Modal, nach WAI-ARIA Authoring Practices und mit echten Screenreader-Tests vor jedem Release.
ARIA-Audit
Bestehende Custom Widgets auf role, State und Tastaturbedienung prüfen
Hyvä-Implementierung
Alpine.js-Komponenten nach APG-Pattern für Comboboxen, Menüs und Dialoge
Screenreader-Testing
Manuelle Tests mit NVDA, VoiceOver und JAWS vor jedem Release
10. Zusammenfassung
Ein Custom Widget mit ARIA barrierefrei zu machen gelingt am zuverlässigsten, wenn man nicht eigene Interaktionslogik erfindet, sondern die etablierten Muster der WAI-ARIA Authoring Practices Guide übernimmt. Für eine Combobox bedeutet das: role="combobox" mit einer separaten role="listbox", aria-expanded synchron zum sichtbaren Zustand, aria-activedescendant zur Markierung der aktiven Option und eine vollständige Tastatursteuerung mit Pfeiltasten, Enter, Escape, Home, End und Typeahead. Roving Tabindex und aria-activedescendant dürfen dabei niemals gemischt werden.
Der entscheidende letzte Schritt bleibt der manuelle Test mit einem echten Screenreader. Automatisierte Tools wie axe-core finden fehlende Attribute und ungültige Werte zuverlässig, aber ob eine Ansage tatsächlich verständlich ist und die Tastaturbedienung sich richtig anfühlt, lässt sich nur mit NVDA, VoiceOver oder JAWS in echten Browsern feststellen. Wer diesen Test vor jedem Release durchführt, statt sich auf CI-Checks allein zu verlassen, vermeidet die häufigsten und teuersten Barrieren in eigenen UI-Komponenten.
Custom Widgets mit ARIA zugänglich machen: Das Wichtigste auf einen Blick
Muster statt Erfindung
Etablierte APG-Patterns für Combobox, Menu, Tabs und Co. übernehmen statt eigene Tastaturlogik zu erfinden.
role, State, Property korrekt setzen
aria-expanded synchron zum sichtbaren Zustand, aria-controls auf die Listbox-ID, aria-activedescendant auf die aktive Option.
Fokus-Management konsistent halten
Roving Tabindex oder aria-activedescendant, niemals gemischt. Bei editierbaren Comboboxen bleibt der Fokus im Eingabefeld.
Mit echtem Screenreader testen
NVDA, VoiceOver und JAWS vor dem Release manuell prüfen. axe-core ist nur die erste Verteidigungslinie.