Semantisches HTML · Inhalte auszeichnen
Tabellen semantisch aufbauen
Eine semantische Tabelle ist eine, in der jede Zelle weiß, zu welcher Überschrift sie gehört: <table> mit <caption>, Kopfzellen als <th scope="col"> oder <th scope="row">, Werte als <td>. Aus einem bloßen Raster aus Text werden damit echte Daten – lesbar für Screenreader, Suchmaschinen und KI-Systeme gleichermaßen.
Tabellen haben trotzdem einen schlechten Ruf. Er stammt aus der Zeit, als <table> fürs Layout missbraucht wurde. Für ihren eigentlichen Zweck – tabellarische Daten – sind sie unverzichtbar und vollkommen richtig. Der Schlüssel liegt darin, die Beziehung zwischen Kopfzellen und Datenzellen explizit auszuzeichnen, statt sie dem Auge zu überlassen.
Das Wichtigste in Kürze
-
<caption>gibt der Tabelle einen Namen und steht direkt nach<table>. -
<th>kennzeichnet Kopfzellen,<td>die Daten – das ist die Grundunterscheidung, an der alles hängt. -
scope="col"bzw.scope="row"legt fest, welchen Bereich eine Kopfzelle beansprucht. Ohne dieses Attribut bleibt der Bezug bei mehr als einer Kopfebene raten. - Layout gehört ins CSS. Tabellen sind für Daten da, Grid und Flexbox fürs Seitenlayout.
- Vorsicht mit CSS: Wer
display: blockoderdisplay: gridauf Tabellenelemente legt, kann die Tabellensemantik zerstören – die häufigste unsichtbare Falle. - WCAG-Bezug: 1.3.1 Info und Beziehungen (Stufe A) verlangt genau diese maschinenlesbaren Beziehungen.
Die Grundregel: Tabellen nur für Daten
Tabellen sind für Informationen gedacht, die sich erst aus dem Zusammenspiel von Zeile und Spalte ergeben – Preise, Termine, Vergleiche, Öffnungszeiten. Fürs Seitenlayout sind sie tabu; dafür gibt es CSS.
Ein guter Test: Streiche gedanklich die Kopfzeile. Ist der Wert „4,5:1“ ohne die Überschriften „Stufe AA“ und „Kontrast“ noch verständlich? Wenn nein, ist es eine Datentabelle – und dann brauchen die Überschriften Auszeichnung. Wenn die „Tabelle“ dagegen nur zwei Textblöcke nebeneinander stellt, ist es Layout und gehört in CSS.
Aufbau einer sauberen Datentabelle
<table>
<caption>
WCAG-Konformitätsstufen im Überblick
</caption>
<thead>
<tr>
<th scope="col">Stufe</th>
<th scope="col">Anspruch</th>
<th scope="col">Kontrast</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">A</th>
<td>Mindestanforderungen</td>
<td>3:1</td>
</tr>
<tr>
<th scope="row">AA</th>
<td>Üblicher Zielwert</td>
<td>4,5:1</td>
</tr>
</tbody>
</table>
Die Bausteine im Einzelnen:
-
<caption>benennt die Tabelle. Sie ist die „Überschrift“ der Tabelle, gehört unmittelbar hinter<table>und wird mit der Tabelle verknüpft – anders als ein davorstehendes<h3>, das nur zufällig darüber steht. Screenreader kündigen die Tabelle damit an, und in einer Liste aller Tabellen der Seite ist die Caption das, was Nutzende zu sehen bekommen. -
<th>kennzeichnet eine Kopfzelle,<td>eine Datenzelle. Kopfzellen stehen nicht nur oben: Die erste Spalte einer Zeile ist genauso eine Überschrift, wenn sie die Zeile benennt. -
scopelegt fest, ob eine Kopfzelle für eine Spalte (col) oder eine Zeile (row) gilt. Dieses Attribut ist der entscheidende Hebel – dazu gleich mehr. -
<thead>,<tbody>,<tfoot>gliedern die Tabelle in Kopf, Rumpf und Fuß. Pflicht sind sie nicht, aber sie erlauben es, den Kopf beim Drucken auf jeder Seite zu wiederholen, und machen das Markup lesbar. Praktischer Hinweis:<tfoot>darf im Quelltext hinter<tbody>stehen und wird trotzdem unten dargestellt – dort gehört die Summenzeile hin. -
<colgroup>/<col>erlauben es, ganze Spalten zu formatieren (etwa eine Spalte hervorzuheben), ohne jede Zelle einzeln mit einer Klasse zu versehen.
Was scope eigentlich festlegt
scope beantwortet für jede Kopfzelle eine einzige Frage: Für welche Zellen bin ich zuständig? Eine Spaltenüberschrift beansprucht alles unter sich, eine Zeilenüberschrift alles rechts von sich. Die Datenzelle im Schnittpunkt gehört zu beiden – daraus setzt Software die vollständige Aussage zusammen.
scope einen Bereich. Die Zelle im Schnittpunkt gehört zu beiden Überschriften – daraus entsteht die Ansage „Anspruch, AA, Üblicher Zielwert“.Vier Werte sind möglich:
| Wert | Gilt für | Typischer Einsatz |
|---|---|---|
col |
alle Zellen der Spalte | Kopfzeile oben |
row |
alle Zellen der Zeile | erste Spalte einer Zeile |
colgroup |
mehrere Spalten | Kopf über mehreren Spalten |
rowgroup |
mehrere Zeilen | Kopf über mehreren Zeilen |
colgroup und rowgroup braucht man selten – sie gehören zu Tabellen mit zwei Kopfebenen, etwa wenn „1. Halbjahr“ über den Spalten „Q1“ und „Q2“ steht. Für den Alltag gilt: col und row decken 95 Prozent der Fälle ab.
Ein Detail, das oft übersehen wird: Browser leiten aus der Position einer <th>
zwar eine Vermutung ab, aber eben nur eine. Sobald eine Tabelle sowohl eine Kopfzeile als auch eine Kopfspalte hat, ist die Vermutung nicht mehr eindeutig.
scope kostet ein Attribut und beendet das Raten – deshalb setze ich es grundsätzlich, auch wenn es „auch so gehen würde“.
Was ein Screenreader daraus macht
In einer optischen Tabelle erkennt das Auge mühelos, dass „AA“ zur Zeile gehört und „Anspruch“ die Spalte überschreibt. Software hat diesen Überblick nicht – sie braucht die Beziehung explizit.
Der Unterschied ist nicht kosmetisch, sondern der ganze Sinn der Sache. Screenreader haben einen eigenen Tabellen-Modus: Mit Tastenkombinationen (bei NVDA etwa Strg + Alt + Pfeiltasten) springt man Zelle für Zelle durch das Raster, und bei jedem Wechsel wird die zugehörige Kopfzelle mitgesprochen – aber nur, wenn es eine gibt. Beim Sprung in eine neue Spalte hört man die neue Spaltenüberschrift, beim Sprung in eine neue Zeile die Zeilenüberschrift. Ohne <th> und scope fehlt diese Orientierung vollständig; übrig bleibt eine Kette zusammenhangloser Werte, bei der man ab Zeile drei nicht mehr weiß, worüber man gerade liest.
Ein vollständiges Praxisbeispiel
So sieht eine Tabelle aus, die alles mitbringt: Caption, Kopfzeile, Kopfspalte, Fußzeile mit Summe – und Zahlen, die sich vergleichen lassen.
<table class="preise">
<caption>
Aufwand je Prüfschritt (Beispielprojekt, Stand Juli 2026)
</caption>
<colgroup>
<col />
<col class="num" />
<col class="num" />
</colgroup>
<thead>
<tr>
<th scope="col">Prüfschritt</th>
<th scope="col">Stunden</th>
<th scope="col">Anteil</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Automatisierte Prüfung</th>
<td>2</td>
<td>13 %</td>
</tr>
<tr>
<th scope="row">Tastaturtest</th>
<td>5</td>
<td>33 %</td>
</tr>
<tr>
<th scope="row">Screenreader-Test</th>
<td>8</td>
<td>54 %</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Gesamt</th>
<td>15</td>
<td>100 %</td>
</tr>
</tfoot>
</table>
Drei Kleinigkeiten, die den Unterschied zwischen „korrekt“ und „gut benutzbar“ ausmachen:
- Zahlen rechtsbündig, Text linksbündig. Untereinanderstehende Ziffern lassen sich vergleichen, wenn die Einer unter den Einern stehen (
text-align: rightüber die<col class="num">). -
Leere Zellen bleiben
<td></td>. Eine Zelle wegzulassen verschiebt alles Folgende. Wenn ein Wert fehlt, gehört ein Strich oder ein kurzes „keine Angabe“ hinein – ein Gedankenstrich allein wird von manchen Screenreadern gar nicht vorgelesen. - Einheiten in die Überschrift, nicht in jede Zelle. „Stunden“ gehört in die Kopfzeile; die Zellen enthalten nur die Zahl. Das kürzt die Ansage und macht die Spalte sortierbar.
Komplexere Tabellen: colspan, rowspan, headers
Bei einfachen Tabellen genügt scope. Sobald Zellen über mehrere Spalten oder Zeilen reichen (colspan, rowspan) oder mehrere Kopfebenen aufeinandertreffen, wird die Zuordnung mehrdeutig – dann kommen id und headers ins Spiel, um jede Datenzelle ausdrücklich mit ihren Kopfzellen zu verknüpfen:
<thead>
<tr>
<td></td>
<th id="h1" colspan="2" scope="colgroup">1. Halbjahr</th>
</tr>
<tr>
<td></td>
<th id="q1" scope="col">Q1</th>
<th id="q2" scope="col">Q2</th>
</tr>
</thead>
<tbody>
<tr>
<th id="web" scope="row">Webprojekte</th>
<td headers="web h1 q1">12</td>
<td headers="web h1 q2">9</td>
</tr>
</tbody>
Das ist mächtig, aber wartungsintensiv: Jede neue Zeile bedeutet neue ids und neue headers-Listen, und ein Tippfehler bleibt unsichtbar. Deshalb ist die Empfehlung von W3C und WebAIM eindeutig – headers/id erst dann, wenn scope nicht mehr reicht. Die Details behandelt der eigene Beitrag Komplexe Datentabellen barrierefrei.
Mein Rat bleibt: Halte Tabellen so einfach wie möglich. Lässt sich eine komplexe Tabelle in zwei klare aufteilen, ist das fast immer die bessere Lösung – für Screenreader und für alle anderen.
Die unsichtbare Falle: CSS kann die Semantik zerstören
Das ist der Fehler, den man am schwersten findet, weil im HTML alles richtig aussieht. Wer Tabellenelemente per CSS auf ein anderes Anzeigemodell umstellt – display: block, display: grid, display: flex, display: contents –, ändert in mehreren Browsern nicht nur die Darstellung, sondern auch die Rolle der Elemente. Aus der Tabelle wird dann ein gewöhnlicher Block, und die mühsam gesetzten scope-Bezüge sind weg.
Klassiker sind das „mobile Umbrechen“ von Tabellen zu Kartenstapeln und CSS-Frameworks, die table { display: block; overflow-x: auto; } als Scroll-Lösung mitbringen:
/* Riskant: hebt in manchen Browsern die Tabellensemantik auf */
table {
display: block;
overflow-x: auto;
}
/* Besser: Der Container scrollt, die Tabelle bleibt eine Tabelle */
.table-scroll {
overflow-x: auto;
}
Die Regel dahinter ist einfach: Layoutänderungen gehören auf einen Container um die Tabelle, nicht auf die Tabelle selbst. Wer die Anzeigemodelle doch umstellt, muss die Rollen mit ARIA (role="table", role="row", role="cell" …) von Hand wiederherstellen – viel Arbeit für ein Problem, das man nicht haben müsste.
Breite Tabellen auf dem Handy
Eine Tabelle mit sechs Spalten passt auf kein Smartphone. Der pragmatische und zugleich barrierefreie Weg ist ein scrollbarer Container, der auch per Tastatur erreichbar ist – denn ein Bereich, der scrollt, aber keinen Fokus bekommen kann, ist mit der Tastatur nicht bedienbar:
<div class="table-scroll" role="region" aria-labelledby="tab-aufwand" tabindex="0">
<table>
<caption id="tab-aufwand">Aufwand je Prüfschritt</caption>
…
</table>
</div>
.table-scroll {
overflow-x: auto;
}
.table-scroll:focus-visible {
outline: 3px solid #0b3d91;
outline-offset: 2px;
}
Drei Zutaten, drei Gründe: tabindex="0" macht den Bereich per Tabulator erreichbar, role="region" sorgt dafür, dass er im Screenreader als benannter Bereich auftaucht, aria-labelledby gibt ihm den Namen der Caption. Die Tabellenstruktur selbst bleibt unangetastet – das Layout-Problem löst der Container, nicht das Markup.
Randnotiz – auch Maschinen lesen Tabellen. Korrekt ausgezeichnete Tabellen helfen nicht nur Screenreadern. Web-Crawler von Suchmaschinen und KI-Crawler (z. B. GPTBot, ClaudeBot) erfassen die Kopf-Daten-Beziehungen und können Werte sauber den richtigen Kategorien zuordnen – genau das ist die Voraussetzung dafür, dass ein Vergleichswert in einer KI-Antwort oder einem Suchergebnis-Snippet richtig herum wiedergegeben wird. Eine als Bild abgespeicherte oder mit
<div>„gemalte“ Tabelle bleibt für sie dagegen weitgehend stumm. Wer Vergleichsdaten hat, die zitiert werden sollen, zeichnet sie als echte Tabelle aus – und ergänzt bei Bedarf strukturierte Daten.
Was das Gesetz dazu sagt
Der Bezug zur WCAG ist direkt: Erfolgskriterium 1.3.1 Info und Beziehungen (Stufe A) verlangt, dass visuell erkennbare Strukturen auch programmatisch verfügbar sind. Eine Tabelle, deren Überschriften man nur sieht, aber nicht auslesen kann, verstößt dagegen – und Stufe A ist die Mindestanforderung, die über BITV und BFSG verbindlich wird. Hinzu kommt 1.4.10 Reflow: Bei 320 Pixel Breite darf nicht in zwei Richtungen gescrollt werden müssen – Datentabellen sind davon ausdrücklich ausgenommen, weshalb der Scroll-Container oben eine zulässige und gute Lösung ist.
Häufige Fehler
- Tabellen fürs Layout statt für Daten.
-
Kopfzellen als
<td>statt<th>– die Beziehung fehlt dann komplett. -
scopeweggelassen, sodass unklar bleibt, ob eine Kopfzelle Zeile oder Spalte beschreibt. -
Keine
<caption>, sodass die Tabelle namenlos in der Tabellenliste steht. -
display: blockauf der Tabelle, wodurch die Semantik verloren geht. -
Das
summary-Attribut verwenden – es ist seit HTML5 überholt. Eine Erläuterung gehört in die<caption>oder in einen Absatz vor der Tabelle. - Tabellen als Screenshot einbinden – für Software vollständig unlesbar.
- Verschachtelte Tabellen – fast immer ein Zeichen dafür, dass zwei getrennte Tabellen die bessere Lösung wären.
Häufige Fragen
Brauche ich immer <thead> und <tbody>?
Pflicht sind sie nicht – der Browser ergänzt <tbody> sogar automatisch. Sie strukturieren die Tabelle aber klar, erlauben das Wiederholen der Kopfzeile beim Druck und erleichtern die Gestaltung. Ich setze sie standardmäßig.
Muss scope an jede Kopfzelle?
Bei einer Tabelle mit nur einer Kopfzeile kommen Browser auch ohne scope zurecht. Sobald es Kopfzeile und Kopfspalte gibt, wird es mehrdeutig. Da das Attribut nichts kostet, setze ich es überall – so bleibt die Tabelle auch dann korrekt, wenn später eine Spalte dazukommt.
<caption> oder eine Überschrift über der Tabelle?
Die <caption> – sie ist mit der Tabelle verknüpft, eine <h3> steht nur daneben. Wenn dir die Standarddarstellung nicht gefällt: Die Caption lässt sich frei gestalten (caption-side, Schriftgröße, Ausrichtung). Sie zu verstecken ist keine gute Idee; wenn sie optisch stören würde, ist meist der Text zu lang.
Wie mache ich eine breite Tabelle mobil bedienbar?
Mit einem scrollbaren Container um die Tabelle, der tabindex="0", role="region" und aria-labelledby bekommt (Beispiel oben). Finger weg von display: block auf der Tabelle selbst. Wenn wirklich viele Spalten nötig sind, hilft oft, unwichtige Spalten mobil auszublenden – aber dann als bewusste Entscheidung, nicht als Nebenwirkung.
Wie sortiere ich eine Tabelle barrierefrei?
Über einen <button> in der Kopfzelle plus aria-sort an der <th>. Der Ablauf und die nötigen Ansagen stehen in Sortierbare Tabellen.
Ist eine Layout-Tabelle heute noch erlaubt?
Erlaubt schon, sinnvoll nicht. Wenn du eine erbst und nicht sofort umbauen kannst, bekommt sie role="presentation" und darf weder <th> noch <caption> enthalten – dann meldet sie sich wenigstens nicht als Datentabelle. Die eigentliche Lösung bleibt CSS.
Wie prüfe ich das schnell?
Der HTML-Validator findet Strukturfehler, und in den Browser-DevTools zeigt der Barrierefreiheits-Bereich die erkannten Rollen – steht dort nicht „table“/„row“/„cell“, greift meist die CSS-Falle von oben. Der verlässlichste Test bleibt: einmal mit Screenreader durch die Tabelle navigieren.
Fazit
Tabellen sind für Daten da, nicht fürs Layout. Mit <caption>, <th> und scope machst du die Beziehung zwischen Überschriften und Werten explizit – und damit für Menschen wie Maschinen verständlich. Zwei Dinge zusätzlich im Kopf behalten: CSS darf die Tabellensemantik nicht abräumen, und breite Tabellen gehören in einen fokussierbaren Scroll-Container. Für mehrdimensionale Fälle hilft Komplexe Datentabellen barrierefrei; den größeren Zusammenhang liefert Was ist semantisches HTML?.
Quellen
- Tables Tutorial (W3C Web Accessibility Initiative – Referenz für Kopfzellen,
scopeund mehrstufige Kopfebenen) - Creating Accessible Tables – Data Tables (WebAIM – Praxisleitfaden inklusive Empfehlung gegen
headers/id)