Eine Tabelle, die im Browser ordentlich aussieht, kann für einen Screenreader trotzdem ein Zahlenbrei sein. Entscheidend ist nicht das Aussehen, sondern ob jede Datenzelle programmatisch weiß, zu welcher Zeile und Spalte sie gehört. Dieser Beitrag zeigt, welche Elemente dafür nötig sind, wann scope reicht und wann headers gebraucht wird, wie man Tabellen mit CSS statt mit alten Attributen gestaltet und wie sie auf dem Smartphone lesbar bleiben.
Tabellen nur für Daten, nicht für Layout
Schon HTML 4.01 riet 1999 davon ab, Tabellen nur zur Seitengestaltung zu verwenden. Der heutige HTML Living Standard formuliert es strenger: Tabellen dürfen nicht als Layouthilfe dienen. Für Spalten, Raster und Kartenlayouts gibt es CSS Grid, Flexbox und Mehrspaltenlayout. Eine Tabelle ist dann richtig, wenn die Daten zwei Dimensionen haben, also jede Zelle erst durch Zeilen- und Spaltenkopf verständlich wird: Preislisten, Fahrpläne, Messwerte, Vergleiche.
Wer eine alte Layouttabelle nicht sofort ersetzen kann, kann sie mit role='presentation' kennzeichnen. Browser und Screenreader behandeln sie dann nicht als Datentabelle. Das ist eine Notlösung, kein Ziel.
Die Bausteine einer Datentabelle
| Element | Aufgabe | Hinweis |
|---|---|---|
caption | Titel der Tabelle | muss das erste Kind von table sein |
colgroup, col | Spalten gruppieren | per CSS wirken dort nur Rahmen, Hintergrund, Breite und visibility |
thead | Kopfzeilen | eine oder mehrere Zeilen mit Spaltenköpfen |
tbody | Datenbereich | mehrere tbody für Gruppen sind erlaubt |
tfoot | Summen, Fußzeilen | darf heute nach tbody stehen; HTML 4.01 verlangte es davor |
th | Kopfzelle | mit scope die Richtung festlegen |
td | Datenzelle | optional headers für komplexe Bezüge |
Das Attribut summary, mit dem man in HTML 4.01 eine Tabelle für Sprachausgaben beschrieb, ist heute obsolet. Eine kurze Erklärung gehört in die caption, in den umgebenden Text oder in ein figure mit figcaption. Die bessere Lösung laut Spezifikation: die Tabelle so aufbauen, dass sie keine Erklärung braucht.
Kopfzellen mit scope verknüpfen
Bei einfachen Tabellen erkennen Screenreader die Zuordnung oft schon aus der Position der th-Zellen. Sobald es Kopfzellen in der ersten Zeile und in der ersten Spalte gibt, sollte scope die Richtung eindeutig machen. Die WAI-Tutorials des W3C unterscheiden vier Fälle:
- Eine Kopfzeile:
thfür die Köpfe reicht. - Kopfzeile und Kopfspalte:
scope='col'undscope='row'. - Unregelmäßige Köpfe, die mehrere Spalten oder Zeilen überspannen:
scope='colgroup'oderscope='rowgroup'zusammen mitcolgroupbzw. mehrerentbody. - Mehrstufige Köpfe, bei denen eine Zelle nicht strikt waagerecht oder senkrecht zugeordnet werden kann:
idan den Köpfen,headersan den Datenzellen.
Verbundene Zellen und headers-Verweise
colspan und rowspan verbinden Zellen über Spalten oder Zeilen. Jede Verbindung erschwert aber die Navigation mit Screenreadern. In den folgenden drei Tabellenzeilen gehört der Wert 42 zu »Berlin«, »2025« und »Q1«; das Attribut headers listet alle drei IDs auf:
<tr>
<td rowspan='2'></td>
<th id='j2025' colspan='2' scope='colgroup'>2025</th>
</tr>
<tr>
<th id='q1' scope='col'>Q1</th>
<th id='q2' scope='col'>Q2</th>
</tr>
<tr>
<th id='berlin' scope='row'>Berlin</th>
<td headers='berlin j2025 q1'>42</td>
<td headers='berlin j2025 q2'>45</td>
</tr>
Die leere Zelle oben links überspannt beide Kopfzeilen und hält die Städtespalte frei. Ehrliche Empfehlung: Wenn eine Tabelle headers an fast jeder Zelle braucht, ist sie meist zu komplex. Zwei einfache Tabellen sind für alle Leser verständlicher als eine verschachtelte.
Was WCAG und Screenreader verlangen
Das WCAG-Erfolgskriterium 1.3.1 »Info and Relationships« (Stufe A) verlangt, dass Struktur und Beziehungen, die visuell vermittelt werden, auch programmatisch erkennbar sind. Für Tabellen heißt das: echte th-Elemente statt fett formatierter td, eine Beschriftung und korrekte Zuordnung. Ein Screenreader liest dann beim Wechsel in eine neue Spalte deren Kopf vor, etwa »Preis pro Monat in Euro, 6,00«. Ohne diese Auszeichnung hört der Nutzer nur »6,00« und muss sich den Bezug merken. Zum Test genügt oft schon die Tastaturnavigation mit NVDA unter Windows oder VoiceOver auf dem Mac durch die Tabelle.
Gestaltung mit CSS statt border und cellpadding
Die Attribute border, cellpadding, cellspacing, bgcolor, width und align an Tabellen sind im HTML Living Standard obsolet. Alles, was sie leisteten, geht mit wenigen CSS-Regeln, und zwar zentral für alle Tabellen einer Website:
table {
border-collapse: collapse; /* ersetzt cellspacing='0' */
width: 100%;
}
th, td {
border: 1px solid #ccc; /* ersetzt border='1' */
padding: 0.5rem 0.75rem; /* ersetzt cellpadding */
text-align: left;
}
td.zahl {
text-align: right;
font-variant-numeric: tabular-nums; /* Ziffern gleich breit */
}
tbody tr:nth-child(even) {
background: #f5f5f5; /* Zebrastreifen */
}
caption {
font-weight: bold;
text-align: left;
padding-bottom: 0.5rem;
}
Zahlen rechtsbündig und mit gleich breiten Ziffern auszurichten, macht Spalten mit Beträgen deutlich leichter vergleichbar. Mehr zu Selektoren wie :nth-child steht im Beitrag zu CSS-Selektoren.
Responsive Tabellen ohne Semantikverlust
Breite Tabellen passen nicht auf ein Smartphone. Die robusteste Lösung ist ein Scroll-Container um die Tabelle: Die Tabelle behält ihre Struktur, der Container scrollt waagerecht. WCAG 2.2 erlaubt das ausdrücklich, denn das Kriterium 1.4.10 »Reflow« nimmt Inhalte aus, die für ihre Bedeutung ein zweidimensionales Layout brauchen, und Datentabellen gehören dazu.
.tabelle-scroll {
overflow-x: auto;
}
.tabelle-scroll:focus-visible {
outline: 2px solid #0057b8;
}
Damit Tastaturnutzer den Bereich erreichen und scrollen können, bekommt der Container tabindex='0', eine Rolle und einen Namen (siehe Beispiel unten). Vorsicht bei Lösungen, die jede Zeile per CSS in eine Karte umbauen: MDN weist darauf hin, dass manche Browser eine Tabelle mit display: block, grid oder flex nicht mehr als Tabelle an Screenreader melden. Wer diesen Weg geht, muss die Rollen per ARIA wiederherstellen. Für lange Tabellen hilft zusätzlich ein mitlaufender Kopf mit position: sticky, erklärt im Beitrag zu position und z-index.
Vollständiges Beispiel zum Übernehmen
Die Werte sind ein fiktives Beispiel. Die Tabelle hat eine Kopfzeile, eine Kopfspalte, eine Fußzeile und den Scroll-Container:
<div class='tabelle-scroll' role='region' aria-labelledby='tarife-titel' tabindex='0'>
<table>
<caption id='tarife-titel'>Hosting-Tarife (Beispielwerte)</caption>
<thead>
<tr>
<th scope='col'>Tarif</th>
<th scope='col'>Speicher in GB</th>
<th scope='col'>Domains</th>
<th scope='col'>Preis pro Monat in Euro</th>
</tr>
</thead>
<tbody>
<tr>
<th scope='row'>Start</th>
<td class='zahl'>10</td>
<td class='zahl'>1</td>
<td class='zahl'>3,00</td>
</tr>
<tr>
<th scope='row'>Plus</th>
<td class='zahl'>50</td>
<td class='zahl'>5</td>
<td class='zahl'>6,00</td>
</tr>
<tr>
<th scope='row'>Pro</th>
<td class='zahl'>200</td>
<td class='zahl'>20</td>
<td class='zahl'>12,00</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope='row'>Durchschnitt</th>
<td class='zahl'>86,7</td>
<td class='zahl'>8,7</td>
<td class='zahl'>7,00</td>
</tr>
</tfoot>
</table>
</div>
Zum Schluss lohnt ein Lauf durch den Nu Html Checker des W3C: Er meldet etwa fehlende Zellen, falsche Verschachtelung oder headers-Verweise auf nicht vorhandene IDs. Wie solche Prüfwerkzeuge einzuordnen sind, beschreibt der Beitrag zu W3C-Webstandards.







