Produktfilter barrierefrei implementieren
AI generated
A11Y
WCAG
Barrierefreiheit · WCAG 2.2 · ARIA · Layered Navigation
Produktfilter (Layered Navigation) barrierefrei implementieren
Tastatur, Live-Regionen und Fokus-Management für Shop-Filter

Produktfilter, die nur mit der Maus bedienbar sind oder nach jeder Auswahl den Fokus verlieren, schließen Nutzer mit Tastatur oder Screenreader faktisch aus. Dieser Artikel zeigt, wie Checkboxen, Radio-Buttons und Preis-Regler in einer Layered Navigation semantisch korrekt ausgezeichnet, per Tastatur bedienbar gemacht und mit Live-Regionen für aktualisierte Ergebniszahlen kombiniert werden, ohne den Fokus beim Neuladen des Produktrasters zu verlieren.

14 Min. Lesezeit WCAG 2.2 AA · ARIA · Fokus-Management Hyvä Theme · Alpine.js · Magento 2

1. Warum barrierefreie Produktfilter über Conversion und Rechtskonformität entscheiden

Produktfilter sind in nahezu jedem Onlineshop die zentrale Steuerung zwischen hunderten Produkten und den wenigen passenden Artikeln. Wenn diese Steuerung nur mit der Maus bedienbar ist, schließt sie Tastaturnutzer, Screenreader-Nutzer und motorisch eingeschränkte Menschen faktisch von der Produktsuche aus. Das ist nicht nur ein rechtliches Risiko unter dem Barrierefreiheitsstärkungsgesetz (BFSG), das seit Juni 2025 auch für B2C-Onlineshops gilt, sondern auch ein handfestes Conversion-Problem: Wer einen Filter nicht bedienen kann, verlässt die Seite, statt zu kaufen.

In der Praxis scheitern Produktfilter an denselben immer wiederkehrenden Stellen: Checkboxen ohne sichtbares Label, Custom-Dropdowns ohne Tastatursteuerung, Ergebniszahlen, die sich unbemerkt ändern, und ein Produktraster, das nach jedem Klick den Fokus zurück auf das Seiten-Body-Element wirft. Jeder dieser Fehler lässt sich mit etablierten WAI-ARIA-Patterns und wenigen Zeilen semantischem HTML beheben, wie die folgenden Abschnitte am Beispiel einer Layered Navigation für Magento und Hyvä zeigen.

2. Semantische Struktur: Fieldset, Legend und Label für Filtergruppen

Jede Filtergruppe, etwa Farbe oder Größe, gehört in ein natives <fieldset>-Element mit einem <legend> als Gruppentitel. Screenreader kündigen die Legende automatisch an, sobald der Fokus die erste Checkbox der Gruppe erreicht, sodass Nutzer wissen, in welchem Kontext sich die Option befindet, ohne jedes einzelne Label erneut vorlesen zu müssen. Ein reines <div> mit einer optisch fett gedruckten Überschrift bietet diese Zuordnung nicht, weil die Gruppierung nur visuell, nicht aber programmatisch existiert.

Jede einzelne Checkbox braucht ein <label>, das über das for-Attribut mit der id der Checkbox verknüpft ist, niemals nur ein umgebendes <span> mit CSS-Styling. Der sichtbare Text sollte die Trefferzahl enthalten, etwa Rot (12), damit Screenreader-Nutzer vor der Auswahl wissen, wie viele Produkte zu erwarten sind. aria-describedby kann zusätzliche Hinweise wie Mehrfachauswahl möglich ergänzen, ohne den primären Label-Text zu verlängern.


<!-- Filter group: fieldset + legend, each checkbox with a bound label -->
<fieldset class="border-0 p-0 m-0 mb-6">
  <legend class="font-bold text-slate-800 text-sm mb-3">Farbe</legend>
  <ul class="space-y-2 list-none m-0 p-0">
    <li>
      <input
        type="checkbox"
        id="filter-color-red"
        name="color[]"
        value="red"
        class="w-4 h-4"
        aria-describedby="filter-color-red-hint"
      >
      <label for="filter-color-red" class="ml-2 text-sm text-slate-700">Rot (12)</label>
      <span id="filter-color-red-hint" class="sr-only">Mehrfachauswahl möglich</span>
    </li>
    <li>
      <input type="checkbox" id="filter-color-blue" name="color[]" value="blue" class="w-4 h-4">
      <label for="filter-color-blue" class="ml-2 text-sm text-slate-700">Blau (7)</label>
    </li>
  </ul>
</fieldset>

3. Tastaturbedienung: Checkboxen, Radio-Buttons und Fokus-Sichtbarkeit

Checkboxen und Radio-Buttons sind von Haus aus mit der Tab-Taste erreichbar und mit der Leertaste beziehungsweise den Pfeiltasten bedienbar, sofern sie native HTML-Elemente bleiben und nicht durch <div>- oder <span>-Konstrukte mit Klick-Handlern ersetzt werden. Das Problem entsteht meist erst, wenn Designer die native Checkbox optisch komplett verstecken (display: none) und durch ein rein visuelles Ersatzelement ersetzen, das keinerlei Tastatur-Interaktion mehr besitzt.

Der sichere Weg ist, die native Checkbox mit einer sr-only-ähnlichen Klasse unsichtbar, aber fokussierbar zu halten und das visuelle Ersatzelement über die :checked- und :focus-visible-Selektoren zu steuern. So bleibt die komplette Tastatur- und Screenreader-Logik der nativen Checkbox erhalten, während das Design frei gestaltbar bleibt. Ein deutlich sichtbarer Fokusring mit ausreichendem Kontrast, mindestens 3:1 laut WCAG 2.2 Kriterium 2.4.11, ist Pflicht, gerade weil viele Shop-Themes den Standard-Browser-Fokusring per CSS entfernen, ohne ihn zu ersetzen.


/* Real checkbox stays in the DOM and keyboard-focusable, only visually hidden */
.filter-checkbox {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Custom visual box driven by the real checkbox state */
.filter-checkbox + .filter-checkbox-box {
  width: 1.25rem;
  height: 1.25rem;
  border: 2px solid #71717a;
  border-radius: 0.25rem;
}

.filter-checkbox:checked + .filter-checkbox-box {
  background-color: #18181b;
  border-color: #18181b;
}

/* Visible, high-contrast focus ring, only on keyboard focus */
.filter-checkbox:focus-visible + .filter-checkbox-box {
  outline: 3px solid #3f3f46;
  outline-offset: 2px;
}

4. Live-Regionen: Aktualisierte Ergebniszahl nach dem Filtern ankündigen

Wenn ein Filter aktiviert wird und sich das Produktraster per AJAX aktualisiert, ändert sich die Ergebniszahl visuell sofort, aber ohne aria-live bleibt diese Änderung für Screenreader-Nutzer unsichtbar. Die Lösung ist eine dedizierte Live-Region, ein Element mit aria-live="polite" und role="status", das ausschließlich die aktuelle Trefferzahl enthält und bei jeder Filteränderung per JavaScript aktualisiert wird. Polite sorgt dafür, dass die Ansage wartet, bis der Screenreader die aktuelle Sprachausgabe beendet hat, statt sie zu unterbrechen.

Wichtig ist, die Live-Region beim initialen Seitenaufbau leer im DOM zu belassen und den Text erst nach der ersten Nutzerinteraktion zu setzen, sonst kündigt der Screenreader die Trefferzahl bereits beim Laden der Seite an, obwohl noch kein Filter aktiv war. Die Region sollte visuell nicht komplett unsichtbar sein, sondern die reguläre Trefferzahl-Anzeige selbst als Live-Region markiert werden, damit sehende und blinde Nutzer dieselbe Information zur gleichen Zeit erhalten.


<!-- Visible result count doubles as the live region, no separate hidden element -->
<p
  id="filter-result-count"
  role="status"
  aria-live="polite"
  aria-atomic="true"
  class="text-sm font-semibold text-slate-700 mb-4"
>
  128 Produkte
</p>

<script>
function updateResultCount(newCount) {
  const region = document.getElementById('filter-result-count');
  // aria-atomic ensures the full sentence is re-announced, not just the diff
  region.textContent = newCount + ' Produkte gefunden';
}
</script>

5. Fokus-Management: Keinen Fokus verlieren, wenn das Produktraster neu rendert

Der wohl größte reale UX-Fehler bei AJAX-Filtern: Nach jedem Klick wird der gesamte Produktraster-Container per innerHTML ersetzt, wodurch der zuvor fokussierte Checkbox-Knoten aus dem DOM entfernt wird. Der Browser wirft den Fokus dann automatisch auf das <body>-Element zurück, und Tastaturnutzer müssen sich mit der Tab-Taste komplett von vorn durch die Seite navigieren, um zum nächsten Filter zu gelangen. Für Screenreader-Nutzer fühlt sich das an, als würde die gesamte Seite neu geladen.

Das korrekte Pattern hält den Filter-Container, der den Fokus besitzt, unangetastet und ersetzt ausschließlich den Produktraster-Bereich. Zusätzlich merkt sich das Skript vor dem Re-Render, welches Element fokussiert war, und stellt den Fokus danach explizit per element.focus() wieder her, falls das ursprüngliche Element doch neu gerendert werden musste. Für den Fall, dass der fokussierte Knoten tatsächlich ersetzt wird, ist ein tabindex="-1" auf einem stabilen Ankerelement wie der Ergebnis-Überschrift ein bewährtes Fallback, um den Fokus programmatisch dorthin zu verschieben.


// Preserve keyboard focus across an AJAX re-render of the product grid
async function applyFilter(url) {
  const activeElement = document.activeElement;
  const activeId = activeElement && activeElement.id;

  const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
  const html = await response.text();

  // Only the grid is replaced, the filter fieldset stays untouched in the DOM
  document.getElementById('product-grid').innerHTML = html;
  updateResultCount(document.querySelectorAll('.product-card').length);

  // Restore focus if the original node still exists after the swap
  const restored = activeId && document.getElementById(activeId);
  if (restored) {
    restored.focus();
    return;
  }

  // Fallback: move focus to a stable, always-present anchor heading
  const anchor = document.getElementById('filter-result-heading');
  anchor.setAttribute('tabindex', '-1');
  anchor.focus();
}

6. ARIA-Patterns für Multiselect-Dropdowns und Filter-Chips

Custom-Dropdowns für Filter, etwa ein Mehrfachauswahl-Menü für Marken, benötigen das WAI-ARIA Listbox- oder Combobox-Pattern, nicht ein selbst gebautes div-Konstrukt ohne Rollen. Ein Button mit aria-haspopup="listbox" und aria-expanded öffnet die Liste, die als ul mit role="listbox" und jedes Element als li mit role="option" sowie aria-selected ausgezeichnet wird. Pfeiltasten navigieren zwischen den Optionen, die Leertaste oder Eingabetaste wählt aus, Escape schließt das Menü und gibt den Fokus an den auslösenden Button zurück.

Aktive Filter, die als entfernbare Chips oberhalb des Produktrasters angezeigt werden, brauchen jeweils einen eigenen Button mit einem eindeutigen aria-label wie Filter Farbe Rot entfernen, statt eines generischen Kreuz-Icons ohne Text-Alternative. Nach dem Entfernen eines Chips sollte der Fokus nicht ins Leere fallen, sondern auf den nächsten verbleibenden Chip oder, falls keiner mehr vorhanden ist, zurück auf die entsprechende Filter-Checkbox in der Fieldset-Gruppe wandern.

7. Preis-Regler (Range-Slider) barrierefrei umsetzen

Ein Preisfilter mit zwei Reglern für Minimum und Maximum lässt sich entweder mit zwei nativen input[type=range]-Elementen oder mit dem WAI-ARIA Slider-Pattern (role="slider") umsetzen. Die native Variante ist deutlich robuster, weil Tastatursteuerung mit Pfeiltasten, Bild-auf, Bild-ab, Pos1 und Ende automatisch funktioniert, ohne dass eigener JavaScript-Code für jede Taste geschrieben werden muss. Zwei überlappende Range-Inputs mit unterschiedlichem z-index sind das gängige Pattern für einen Doppel-Slider, wobei jeder Regler einen eigenen aria-label wie Mindestpreis beziehungsweise Höchstpreis trägt.

Wird stattdessen eine komplett individuell gestylte Slider-Komponente gebaut, müssen aria-valuemin, aria-valuemax und aria-valuenow bei jeder Bewegung aktuell gehalten werden, zusätzlich zu einem aria-valuetext, das den Wert lesbar formatiert, etwa 49 Euro statt nur der rohen Zahl 49. Ohne aria-valuetext liest mancher Screenreader nur die reine Ziffer vor, was bei Währungs- oder Prozentwerten missverständlich ist. Der Fokusring des Reglers muss bei jeder Tastatureingabe sichtbar bleiben, auch während sich der Wert in Echtzeit ändert.

8. Praktisches Pattern: Layered Navigation mit Alpine.js in Hyvä

In Hyvä lässt sich eine barrierefreie Layered Navigation vollständig mit einer Alpine.js-Komponente umsetzen, ohne zusätzliche JavaScript-Bibliotheken zu laden. Die Komponente hält den Ladezustand, die aktuelle Trefferzahl und eine Referenz auf das zuletzt fokussierte Element in ihrem lokalen State und aktualisiert nach jedem AJAX-Request sowohl den Produktraster als auch die Live-Region mit der neuen Trefferzahl, ohne den restlichen Seitenbereich neu zu rendern.

Der entscheidende Kniff im folgenden Beispiel: Der x-init-Hook merkt sich das aktive Element vor dem Fetch-Aufruf, und nach dem Einfügen der neuen Produktkarten prüft die Komponente, ob dieses Element noch im DOM existiert. Ist das der Fall, bleibt der Fokus unverändert. Ist es verschwunden, wandert der Fokus explizit auf die Trefferzahl-Überschrift, die dafür ein tabindex="-1" trägt und so programmatisch fokussierbar, aber nicht Teil der normalen Tab-Reihenfolge ist.


// Hyva_Theme::product/layered-nav.phtml - Alpine.js component
document.addEventListener('alpine:init', () => {
  Alpine.data('layeredNav', () => ({
    loading: false,
    resultCount: 0,

    async toggleFilter(event, url) {
      this.loading = true;
      const lastFocusedId = document.activeElement.id;

      try {
        const response = await fetch(url, {
          headers: { 'X-Requested-With': 'XMLHttpRequest' }
        });
        const data = await response.json();

        // Replace only the grid, the fieldset filters stay mounted
        document.getElementById('product-grid').innerHTML = data.gridHtml;
        this.resultCount = data.count;

        // Announce the new count through the existing live region
        this.$refs.liveRegion.textContent = data.count + ' Produkte gefunden';

        // Restore focus to the same control if it still exists
        this.$nextTick(() => {
          const restored = document.getElementById(lastFocusedId);
          if (restored) {
            restored.focus();
          } else {
            this.$refs.resultHeading.focus();
          }
        });
      } finally {
        this.loading = false;
      }
    }
  }));
});

9. Testing und Vergleich: Screenreader, axe-core und typische Fehler

Automatisierte Tools wie axe-core oder Lighthouse-Accessibility-Audits finden zuverlässig fehlende Labels, unzureichende Kontraste und fehlende ARIA-Attribute, aber sie erkennen keinen Fokusverlust nach einem AJAX-Update und keine falsch formulierten Live-Region-Ansagen. Deshalb braucht jede Layered Navigation zusätzlich einen manuellen Test mit einem echten Screenreader: NVDA mit Firefox unter Windows oder VoiceOver mit Safari unter macOS sind die gängigsten Kombinationen für den deutschen Markt.

Beim manuellen Test wird ausschließlich mit der Tastatur gefiltert, ganz ohne Maus, und geprüft, ob jede Ansage der Live-Region inhaltlich korrekt und rechtzeitig erfolgt, ob der Fokus nach jeder Interaktion an einer sinnvollen Stelle bleibt, und ob sich Custom-Dropdowns mit Pfeiltasten, Escape und Eingabetaste wie erwartet verhalten. Die folgende Tabelle vergleicht typische Implementierungsfehler mit dem barrierefreien Gegenstück.

Bereich Nicht barrierefrei Barrierefreies Pattern Vorteil
Filtergruppe div mit fett gedruckter Überschrift fieldset + legend Gruppenkontext wird programmatisch angesagt
Checkbox-Ersatz div mit onclick, display: none Checkbox Native Checkbox, sr-only + :checked Tastatur- und Screenreader-Logik bleibt erhalten
Ergebniszahl Nur visuelle Textänderung role="status" aria-live="polite" Screenreader kündigt neue Trefferzahl an
Grid-Update innerHTML des ganzen Containers Fokus merken, gezielt wiederherstellen Kein Zurückfallen des Fokus auf body
Preis-Regler Eigenbau ohne aria-valuenow input[type=range] oder role="slider" + aria-valuetext Wert wird korrekt und verständlich vorgelesen

Mironsoft

Barrierefreiheits-Audits und WCAG-konforme Umsetzung für Magento- und Hyvä-Shops

Produktfilter, die wirklich für alle bedienbar sind?

Wir prüfen eure Layered Navigation auf Tastaturbedienbarkeit, Live-Regionen und Fokus-Management und setzen die notwendigen Anpassungen direkt im Hyvä-Theme um, damit euer Shop das BFSG erfüllt und für alle Kunden nutzbar bleibt.

Barrierefreiheits-Audit

Tastatur-, Screenreader- und axe-core-Prüfung eurer Filter und Formulare

Hyvä-Umsetzung

ARIA-Patterns, Live-Regionen und Fokus-Management direkt im Theme implementieren

BFSG-Beratung

Rechtliche Anforderungen einordnen und priorisierten Umsetzungsplan erstellen

10. Zusammenfassung

Barrierefreie Produktfilter lösen immer dasselbe Grundproblem: Nutzer, die nicht mit der Maus arbeiten können oder wollen, dürfen nicht von der Produktsuche ausgeschlossen werden. Semantisches HTML mit fieldset und legend, native statt nachgebauter Checkboxen mit sichtbarem Fokusring, eine aria-live-Region für die Trefferzahl und ein Fokus-Management, das den zuletzt fokussierten Knoten über AJAX-Updates hinweg erhält, bilden zusammen ein vollständiges, barrierefreies Pattern für die Layered Navigation.

Der größte Hebel liegt darin, diese Patterns konsequent für jede Filterkomponente anzuwenden, nicht nur für die einfachen Checkbox-Listen. Custom-Dropdowns, Filter-Chips und Preis-Regler brauchen jeweils eigene ARIA-Rollen und eine eigene Tastatursteuerung. Automatisierte Tools wie axe-core finden einen Großteil der offensichtlichen Fehler, aber erst der manuelle Test mit einem echten Screenreader zeigt zuverlässig, ob Live-Regionen und Fokus-Management im Zusammenspiel tatsächlich funktionieren.

Barrierefreie Produktfilter, das Wichtigste auf einen Blick

Semantische Struktur

fieldset + legend pro Filtergruppe, jede Checkbox mit gebundenem label statt reinem CSS-Styling.

Live-Region

role="status" mit aria-live="polite" für die Trefferzahl, aktualisiert bei jeder Filteränderung.

Fokus-Management

Aktives Element vor dem Re-Render merken, danach wiederherstellen oder gezielt auf eine stabile Überschrift verschieben.

Testing

axe-core für automatisierte Basisprüfung, NVDA/VoiceOver für manuellen Test von Live-Regionen und Fokus.

11. FAQ: Produktfilter barrierefrei implementieren

1Warum müssen Produktfilter barrierefrei sein?
Sonst schließen sie Tastatur- und Screenreader-Nutzer von der Produktsuche aus. Seit Juni 2025 verlangt das BFSG barrierefreie B2C-Onlineshops, und nicht bedienbare Filter sind ein direktes Conversion-Hindernis.
2Warum reicht ein div mit fetter Überschrift nicht?
Die Gruppierung existiert nur visuell. fieldset mit legend wird programmatisch angesagt, sobald der Fokus die erste Checkbox erreicht, ein div bietet diese Zuordnung nicht.
3Darf ich native Checkboxen optisch verstecken?
Nur mit einer sr-only-ähnlichen Klasse, die fokussierbar bleibt. display: none entfernt die Checkbox aus der Tab-Reihenfolge und der Tastaturbedienung.
4Was macht eine Live-Region für die Ergebniszahl?
role="status" mit aria-live="polite" wird automatisch vorgelesen, sobald sich der Inhalt ändert. Blinde Nutzer erfahren so sofort die neue Trefferzahl.
5Warum polite statt assertive?
Polite wartet, bis die aktuelle Sprachausgabe endet. Assertive würde jede laufende Ansage sofort abbrechen, was bei einer Trefferzahl unnötig aufdringlich wäre.
6Warum verliert das Grid oft den Fokus?
Der Container wird per innerHTML ersetzt und der fokussierte Knoten damit entfernt. Der Browser wirft den Fokus dann auf das body-Element zurück.
7Wie verhindere ich Fokusverlust beim Grid-Update?
Aktives Element vor dem Re-Render merken, nach dem Update auf Existenz prüfen und Fokus wiederherstellen oder auf eine stabile Überschrift mit tabindex="-1" verschieben.
8Welches ARIA-Pattern für Multiselect-Dropdowns?
Das Listbox-Pattern: Button mit aria-haspopup="listbox", Liste mit role="listbox", Optionen mit role="option" und aria-selected. Pfeiltasten navigieren, Escape schließt.
9Wie setze ich einen barrierefreien Preis-Regler um?
Am robustesten mit zwei nativen input[type=range]. Bei Eigenbau mit role="slider" müssen aria-valuemin, aria-valuemax, aria-valuenow und aria-valuetext gepflegt werden.
10Reicht axe-core zum Testen von Filtern aus?
Nein. axe-core findet fehlende Labels und Kontrastfehler, aber keinen Fokusverlust und keine falschen Live-Region-Ansagen. Manueller Test mit NVDA oder VoiceOver ist zusätzlich nötig.