Komponenten · Medien & Daten
Sortierbare & filterbare Tabellen barrierefrei
Eine sortierbare Tabelle wird barrierefrei durch drei Bausteine: einen echten <button> im Spaltenkopf als Auslöser, das Attribut aria-sort am <th> mit genau einem aktiven Wert pro Tabelle, und eine Live-Region, die das Ergebnis ansagt. Der visuelle Pfeil ist dabei Dekoration und wird mit aria-hidden="true" versteckt.
Sobald eine Tabelle sortier- und filterbar wird, kommen zur soliden Tabellen-Semantik drei neue Fragen dazu: Wie bedient man die Sortierung ohne Maus? Woher weiß ein Screenreader, wonach gerade sortiert ist? Und wer sagt Bescheid, wenn der Filter die Liste umbaut? Für alle drei gibt es etablierte Antworten – das Sortable-Table-Beispiel aus dem ARIA Authoring Practices Guide plus eine Statusmeldung.
Das Wichtigste in Kürze
-
Der Auslöser ist ein
<button>im<th>, nicht der<th>mit Klick-Handler. Nur so gibt es Fokus, Enter und Leertaste vom Browser. -
aria-sortsitzt am<th>, nie am Button. Zulässig sindascending,descending,noneundotherfür Sonderfälle – und nur eine Spalte trägt einen aktiven Wert. - Der Sortierpfeil ist Deko und bekommt
aria-hidden="true". Die Richtung trägt das Attribut, nicht das Symbol. -
Der Hinweis auf die Sortierfunktion steht einmal in der
<caption>, nicht alsaria-describedbyan jedem Button – sonst hört man ihn in jeder Spalte erneut. - Nach dem Sortieren bleibt der Fokus auf dem geklickten Button, und eine
role="status"-Meldung nennt Sortierung und Trefferzahl. - Filter sind normale Formulare. Beim Auswählen darf kein Kontextwechsel passieren (3.2.2), die Trefferliste aktualisiert sich daneben.
- Breite Tabellen dürfen im eigenen Container waagerecht scrollen – das ist die legitime Ausnahme der Reflow-Regel, braucht aber
tabindex="0"und einen Namen am Scroll-Container. - Sticky Spaltenköpfe können den Fokus verdecken. Seit WCAG 2.2 ist das ein eigener Verstoß (2.4.11).
Die Basis: eine echte Tabelle
Ohne Fundament kein Muster: <table> mit <caption>, <th scope="col"> für die Spaltenköpfe – alles wie in
Tabellen semantisch aufbauen
beschrieben (WCAG 1.3.1). Ein div-Grid mit Tabellen-Optik disqualifiziert sich hier von selbst: Es verliert die Zellen-Navigation, mit der Screenreader-Nutzer Tabellen erkunden, und damit den einzigen Grund, überhaupt eine Tabelle zu bauen.
Sortierung: Button im Kopf, Zustand am Kopf
<table>
<caption>
Offene Rechnungen
<span class="hinweis">Spalten lassen sich über die Schaltflächen in der Kopfzeile
sortieren.</span>
</caption>
<thead>
<tr>
<th scope="col" aria-sort="ascending">
<button type="button">
Fälligkeit
<span aria-hidden="true">▲</span>
</button>
</th>
<th scope="col" aria-sort="none">
<button type="button">Betrag <span aria-hidden="true">↕</span></button>
</th>
<th scope="col">Kunde</th>
</tr>
</thead>
<tbody>…</tbody>
</table>
Die Regeln dahinter:
-
Der Auslöser ist ein
<button>im<th>– nicht der<th>selbst mit Klick-Handler. Nur so gibt es Fokus, Enter und Leertaste gratis (Buttons vs. Links). -
aria-sortsitzt am<th>und kennt vier Werte:ascending,descending,noneundother(für Sortierungen, die weder auf- noch absteigend sind). Immer nur eine Spalte trägt einen aktiven Wert; beim Umsortieren wird er von der alten Spalte entfernt. Screenreader sagen dann „Fälligkeit, Schaltfläche, aufsteigend sortiert, Spaltenkopf“ an – das erfüllt 4.1.2 Name, Rolle, Wert. - Der Pfeil ist Deko (
aria-hidden="true"). Ein Symbol allein wäre zu wenig: Richtung nur über ein Zeichen ohne Zustands-Attribut scheitert an 1.3.1; zusätzlich braucht das Icon 3:1 Kontrast. -
Nicht sortierbare Spalten bekommen keinen Button und kein
aria-sort. Einaria-sort="none"an einer Spalte, die sich nicht sortieren lässt, ist eine falsche Zusage.
Nach dem Umsortieren bleibt der Fokus auf dem geklickten Button – kein Sprung, kein Neuladen-Gefühl (2.4.3). Und die Sortierung wird nur durch Klick oder Tastendruck ausgelöst, nie durch reines Fokussieren: Eine Tabelle, die sich beim Durchtabben von selbst umsortiert, verstößt gegen 3.2.1 Bei Fokus.
Der Hinweis gehört in die <caption>
Ein Detail aus dem APG-Beispiel, das viel Ansage-Lärm vermeidet: Die Erklärung, dass sich Spalten sortieren lassen, steht einmal in der Tabellenbeschriftung – nicht als aria-describedby an jedem Sortier-Button.
Der Unterschied ist hörbar. Bei fünf sortierbaren Spalten bedeutet die aria-describedby-Variante, dass derselbe Satz fünfmal vorgelesen wird, jedes Mal wenn der Fokus eine Kopfzelle betritt. In der <caption> kommt er genau einmal, nämlich beim Betreten der Tabelle – und zwar exakt in dem Moment, in dem die Information gebraucht wird.
Filter: ein Formular wie jedes andere – plus Ansage
Filter sind Formulare: sichtbare Labels, Gruppen in fieldset/legend, und kein Kontextwechsel beim Auswählen (3.2.2) – die Trefferliste darf sich daneben aktualisieren, aber nichts darf den Fokus stehlen oder die Seite wechseln.
Das Ergebnis braucht eine Statusmeldung (4.1.3):
<p role="status" id="treffer-status">
17 Rechnungen gefunden, gefiltert nach „offen“, sortiert nach Fälligkeit aufsteigend.
</p>
Die Live-Region existiert von Anfang an im DOM; das Skript schreibt nach jedem Filter- oder Sortierlauf den neuen Text hinein. Zwei Praxisregeln dazu: Sichtbar anzeigen schadet nie – die Trefferzahl hilft allen. Und bei Live-Filterung während der Eingabe drosseln: Wer nach jedem Tastendruck eine Meldung erzeugt, produziert Dauerrauschen statt Information. Ungefähr 500 Millisekunden Wartezeit nach der letzten Eingabe sind ein brauchbarer Wert.
Wenn ein Filter die Tabelle auf null Treffer reduziert, gehört das ausdrücklich gesagt – „Keine Rechnungen entsprechen den Filtern. Filter zurücksetzen.“ Eine leere Tabelle ohne Text ist für Screenreader-Nutzer nicht von einem Ladefehler zu unterscheiden.
Breite Tabellen, Sticky-Köpfe und Zoom
Breite Datentabellen sind die legitime Ausnahme der Reflow-Regel: Sie dürfen im eigenen Container waagerecht scrollen. Damit das auch per Tastatur geht, braucht der Container einen Fokus und einen Namen:
<div role="region" aria-label="Offene Rechnungen, waagerecht scrollbar" tabindex="0">
<table>…</table>
</div>
Die Seite drumherum muss trotzdem umbrechen. Und ein Detail, das erst mit WCAG 2.2 scharf geworden ist: Ein sticky Spaltenkopf – also eine Kopfzeile, die beim Scrollen oben stehen bleibt – kann die gerade fokussierte Zeile verdecken. Genau das verbietet 2.4.11 Fokus nicht verdeckt. Die Lösung ist eine Zeile CSS:
.tabellen-container {
scroll-padding-block-start: 3.5rem; /* Höhe des sticky Kopfes */
}
Bei sehr großen Tabellen mit virtuellem Scrolling – also nur wenigen im DOM vorhandenen Zeilen – gehören zusätzlich aria-rowcount an die Tabelle und aria-rowindex an jede Zeile. Sonst sagt der Screenreader „Zeile 3 von 20“, obwohl es 4.000 sind. Das ist Spezialfall-Technik, aber der einzige Weg, Nutzern die tatsächliche Größe mitzuteilen.
Der Fallback ohne JavaScript
Die robusteste sortierbare Tabelle ist die, die serverseitig sortiert. Jeder Spaltenkopf enthält dann keinen Button, sondern einen Link:
<th scope="col" aria-sort="ascending">
<a href="?sort=faellig&richtung=desc">Fälligkeit <span aria-hidden="true">▲</span></a>
</th>
Das funktioniert ohne Skript, ist verlinkbar, teilbar und für Suchmaschinen erreichbar. Der Preis: ein Seitenwechsel pro Sortierung, und der Fokus landet danach am Seitenanfang. Wer beides haben will, nimmt Links als Basis und fängt sie per JavaScript ab – dann bleibt der Fokus stehen, und ohne Skript funktioniert es weiterhin.
Ein Zero-JS-Zwang folgt aus den WCAG nicht. Wo eine JS-Lösung vollständig tastatur- und screenreader-tauglich ist, ist sie konform.
Wann role="grid"?
Eine sortierbare Tabelle bleibt eine <table>. role="grid" kommt erst ins Spiel, wenn Nutzer sich zellenweise mit Pfeiltasten durch die Daten bewegen sollen – wie in einer Tabellenkalkulation. Dann übernimmst du das komplette Tastaturmodell: Pfeiltasten, Home/Ende, Strg+Home, Roving Tabindex, und die Screenreader-eigene Tabellennavigation ist ausgeschaltet.
Das ist selten, was man wirklich will. Für „ich möchte sortieren und filtern“ ist es zu viel – da würd ich bei der eingebauten Tabellennavigation der Screenreader bleiben. Die ist den Nutzern vertraut und kostet nichts.
Häufige Fehler
-
Klick-Handler direkt auf
<th>– ohne Button keine Tastatur. -
aria-sortfehlt oder klebt auf allen Spalten gleichzeitig. - Lautlose Updates – die Liste ändert sich, niemand erfährt es (4.1.3).
-
aria-describedbymit dem Sortierhinweis an jedem Button statt einmal in der<caption>. - Filter löst Seiten-Reload aus und wirft den Fokus an den Anfang.
- Nur-Icon-Sortierpfeile ohne Namen und ohne Zustand.
- Null Treffer ohne Meldung – nicht von einem Fehler zu unterscheiden.
- Sticky Kopfzeile über dem Fokus – Verstoß gegen 2.4.11.
- CSS-Grid-Pseudotabelle – Semantik weg, Zellen-Navigation weg.
Häufige Fragen
Muss die Sortierung auch ohne JavaScript funktionieren?
Nein, das verlangen die WCAG nicht. Ideal ist trotzdem ein Fallback über echte Links (?sort=betrag) mit Server-Sortierung – robust, verlinkbar und crawlbar. Wo das nicht geht, gilt: Die JS-Variante muss vollständig tastatur- und screenreader-tauglich sein.
Wie kündige ich die Sortier-Funktion an?
Der Button im Kopf macht sie entdeckbar; der erklärende Satz gehört in die <caption> („Spalten lassen sich per Schaltfläche in der Kopfzeile sortieren“). Wichtiger als die Ankündigung ist der korrekte Zustand via aria-sort – er beantwortet die Frage, die beim Betreten der Tabelle wirklich zählt.
Und richtig große Tabellen mit Paginierung?
Dann kommt das Muster Pagination & „Mehr laden“ dazu – inklusive Fokus-Management beim Nachladen. Für mehrdimensionale Monster gilt zuerst komplexe Datentabellen.
Soll die Tabelle beim Laden vorsortiert sein?
Ja, mit einer sinnvollen Voreinstellung und dem passenden aria-sort-Wert. Eine unsortierte Tabelle, deren Kopf trotzdem Pfeile zeigt, weckt die Erwartung eines Zustands, der nicht existiert.
Zählt „nach Klick auf den Kopf sortiert“ als Kontextwechsel?
Nein. Ein Kontextwechsel im Sinne von 3.2.2 wäre ein Seitenwechsel, ein Fokussprung oder ein neues Fenster. Neu geordnete Zeilen innerhalb derselben Tabelle sind eine Inhaltsänderung – die aber angesagt werden muss.
Fazit
Sortierbare Tabellen sind drei Zutaten: echte Tabellen-Semantik, ein <button> im Spaltenkopf mit aria-sort am <th>, und eine Live-Region, die Filter- und Sortier-Ergebnisse ansagt. Damit sind Tastatur, Zustand und Statusmeldung abgedeckt.
Der Rest sind Details, die man einmal einrichtet: der Funktionshinweis in der <caption>, das scroll-padding unter dem sticky Kopf, die gedrosselte Meldung beim Live-Filtern. Und wenn die Daten visualisiert werden, gehören sie in barrierefreie Diagramme – mit derselben Tabelle als Alternative daneben.
Quellen
- Sortable Table Example (W3C ARIA APG – aria-sort am th, Button im Kopf, Hinweis in der caption)
- aria-sort (WAI-ARIA 1.2) (W3C – die zulässigen Werte und die Regel, dass nur eine Spalte einen aktiven Wert trägt)