Custom Widgets mit ARIA zugänglich machen
A11Y
WCAG
Barrierefreiheit · WAI-ARIA · Custom Widgets · Hyvä Theme
Custom Widgets mit ARIA zugänglich machen
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.

17 Min. Lesezeit role · aria-expanded · aria-activedescendant · Tastatur NVDA · VoiceOver · JAWS · Alpine.js

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.

11. FAQ: Custom Widgets mit ARIA zugänglich machen

1Wann sollte ich ein natives HTML-Element statt eines Custom Widgets verwenden?
Immer wenn select, input list oder button das gewünschte Verhalten und Design abdecken. Native Elemente bringen Tastatursteuerung, Fokus-Management und Screenreader-Semantik kostenlos mit.
2Was ist die WAI-ARIA Authoring Practices Guide (APG)?
Ein von der W3C-Arbeitsgruppe gepflegter Katalog getesteter Interaktionsmuster für Combobox, Tabs, Dialog und mehr, mit exakten Vorgaben für Rollen, Zustände und Tastaturbedienung.
3Was bedeutet aria-expanded genau?
Zeigt an, ob ein zusammenklappbares Element aktuell sichtbar oder verborgen ist. Muss exakt synchron mit dem tatsächlichen CSS-Sichtbarkeitszustand gesetzt werden.
4Wofür wird aria-activedescendant verwendet?
Zeigt per ID auf das aktuell hervorgehobene Kindelement, während der DOM-Fokus im Eingabefeld bleibt. Screenreader lesen die aktive Option vor, ohne dass sich der echte Fokus bewegt.
5Roving Tabindex oder aria-activedescendant, was ist der Unterschied?
Roving Tabindex verschiebt den echten DOM-Fokus. aria-activedescendant verschiebt nur eine ARIA-Referenz, der Fokus bleibt im Container. Für editierbare Comboboxen ist aria-activedescendant richtig.
6Welche Tasten muss ein Combobox mindestens unterstützen?
Pfeil-runter/hoch zur Navigation, Enter zur Auswahl, Escape zum Schließen, Home und End für Anfang und Ende der Liste. Typeahead verbessert die Bedienung zusätzlich.
7Reicht automatisiertes Testing mit axe-core aus?
Nein. Automatisierte Tools erkennen nur statische Regelverstöße. Ob Ansagen verständlich sind und die Tastaturbedienung funktioniert, zeigt nur ein echter Screenreader-Test.
8Welche Screenreader sollte ich zum Testen verwenden?
Mindestens NVDA mit Firefox und VoiceOver mit Safari, idealerweise ergänzt um JAWS mit Chrome, da JAWS in vielen Unternehmensumgebungen noch verbreitet ist.
9Was ist der häufigste Fehler bei selbstgebauten Dropdowns?
Ein Eingabefeld ohne sichtbares Label, bei dem nur Platzhaltertext als Beschriftung dient. Placeholder verschwinden beim Tippen und werden oft nicht vorgelesen.
10Muss aria-label verwendet werden, auch wenn ein sichtbares Label da ist?
Bei korrekt verknüpftem label-Element ist zusätzliches aria-label meist überflüssig und kann Konflikte erzeugen. aria-labelledby lohnt sich bei mehrteiligen Beschriftungen.