ul, ol, dl und th mit scope statt div-Suppe
Wer Listen als gestapelte div-Elemente und Tabellen als reines Layoutwerkzeug missbraucht, macht Inhalte für Screenreader-Nutzer unbrauchbar. Mit ul, ol, dl für echte Aufzählungen und th mit scope sowie caption für echte Tabellendaten werden Beziehungen zwischen Inhalten programmatisch erkennbar, navigierbar und WCAG-konform, ohne visuelles Design zu opfern.
Inhaltsverzeichnis
- 1. Warum semantische Auszeichnung über Zugänglichkeit entscheidet
- 2. ul, ol und dl: die drei Listentypen von HTML
- 3. Das Problem mit div-Suppe statt echter Listen
- 4. Navigation, Warenkorb und Produktraster als Listen
- 5. Tabellen-Grundlagen: th, scope und caption
- 6. Komplexe Tabellen: headers, id und mehrstufige Header
- 7. Warum Tabellen niemals für visuelles Layout dienen sollten
- 8. Screenreader-Test: wie NVDA und VoiceOver Listen und Tabellen vorlesen
- 9. Praxisbeispiel: barrierefreie Produktvergleichstabelle
- 10. Zusammenfassung
- 11. FAQ
1. Warum semantische Auszeichnung über Zugänglichkeit entscheidet
Screenreader und andere assistive Technologien verlassen sich nicht auf visuelles Design, sondern auf die semantische Struktur des HTML-Dokuments. Eine Liste, die nur durch Zeilenumbrüche und CSS-Abstände wie eine Liste aussieht, ist für einen Screenreader schlicht eine Ansammlung unzusammenhängender div-Elemente. Erst <ul>, <ol> oder <dl> teilen der Accessibility-API mit, dass hier eine Gruppe zusammengehöriger Elemente vorliegt, die als Einheit navigiert werden kann. Genau diese programmatisch erkennbare Struktur fordern die WCAG-Erfolgskriterien 1.3.1 (Info and Relationships) und 4.1.2 (Name, Role, Value) explizit ein.
Der Unterschied ist für sehende Nutzer unsichtbar, für Screenreader-Nutzer aber fundamental: NVDA kündigt eine korrekt ausgezeichnete Liste mit „Liste mit 8 Einträgen" an und erlaubt die Navigation von Eintrag zu Eintrag per Taste. Bei einer div-Konstruktion fällt diese Ankündigung komplett weg, und der Nutzer muss sich Element für Element durch unstrukturierten Text vorarbeiten, ohne zu wissen, wie viele Einträge noch folgen oder wo eine logische Gruppe endet. Bei Tabellen gilt dasselbe Prinzip in verschärfter Form: Ohne <th scope> kann ein Screenreader eine Zelle nicht mehr ihrer Zeilen- oder Spaltenüberschrift zuordnen, und komplexe Datentabellen werden faktisch unlesbar.
2. ul, ol und dl: die drei Listentypen von HTML
HTML bietet drei native Listentypen, die jeweils eine unterschiedliche semantische Aussage treffen. <ul> (unordered list) signalisiert, dass die Reihenfolge der Einträge keine inhaltliche Bedeutung hat, etwa bei einer Liste von Produktmerkmalen oder Navigationslinks. <ol> (ordered list) hingegen zeigt an, dass die Reihenfolge relevant ist, zum Beispiel bei Schritt-für-Schritt-Anleitungen im Checkout-Prozess oder einer Rangliste. <dl> (description list) bildet Schlüssel-Wert-Paare ab und eignet sich hervorragend für Produktattribute wie Material, Größe und Gewicht, die als <dt>/<dd>-Paare ausgezeichnet werden.
Die Wahl des richtigen Listentyps ist keine Stilfrage, sondern beeinflusst direkt, was ein Screenreader ankündigt. VoiceOver liest bei einer <ol> automatisch die Positionsangabe „Eintrag 3 von 7" vor, was bei einer nummerierten Anleitung wichtige Orientierung gibt. Bei einer <ul> entfällt diese Positionsangabe zugunsten der reinen Ankündigung „Listeneintrag". Wichtig: Jedes direkte Kind von <ul> oder <ol> muss ein <li>-Element sein, alles andere verletzt die HTML-Spezifikation und führt in der Accessibility-Tree-Berechnung zu inkonsistentem Verhalten zwischen Browsern.
<!-- ul: Reihenfolge irrelevant, e.g. product features -->
<ul class="space-y-2">
<li>Wasserdicht bis 30 Meter</li>
<li>Titan-Gehäuse, 42 mm Durchmesser</li>
<li>Saphirglas kratzfest</li>
</ul>
<!-- ol: Reihenfolge relevant, e.g. checkout steps -->
<ol class="list-decimal pl-6 space-y-1">
<li>Warenkorb prüfen</li>
<li>Versandadresse eingeben</li>
<li>Zahlungsart wählen</li>
<li>Bestellung bestätigen</li>
</ol>
<!-- dl: key-value pairs, e.g. product attributes -->
<dl class="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1">
<dt class="font-semibold">Material</dt>
<dd>Titan, gebürstet</dd>
<dt class="font-semibold">Gewicht</dt>
<dd>68 g</dd>
<dt class="font-semibold">Wasserdichtigkeit</dt>
<dd>30 m (3 ATM)</dd>
</dl>
3. Das Problem mit div-Suppe statt echter Listen
„Div-Suppe" bezeichnet den weitverbreiteten Anti-Pattern, listenartige Inhalte ausschließlich mit verschachtelten <div>-Elementen und CSS-Klassen wie flex flex-col gap-2 nachzubauen. Visuell ist das Ergebnis von einer echten Liste nicht zu unterscheiden, weil Tailwind-Utility-Klassen dieselben Abstände und Ausrichtungen erzeugen können wie native Listenelemente. Für die Accessibility-API existiert der semantische Zusammenhang aber schlicht nicht: Der Accessibility Tree meldet lediglich eine generische „group"-Rolle oder gar keine Rolle, und die Anzahl der Einträge bleibt dem Nutzer verborgen.
Dieses Muster entsteht in der Praxis oft schleichend: Ein Entwickler kopiert eine bestehende Komponente, die zufällig divs statt li verwendet, und multipliziert das Problem über weitere Komponenten hinweg. Automatisierte Tools wie axe-core oder Lighthouse erkennen div-Suppe nicht zuverlässig als Fehler, weil technisch kein WCAG-Kriterium direkt verletzt wird, solange keine ARIA-Rolle falsch gesetzt ist. Genau deshalb ist manuelles Review mit Screenreader unverzichtbar. Die Regel ist einfach: Sobald Inhalte eine Aufzählung, eine Reihenfolge oder eine Gruppe zusammengehöriger Einträge darstellen, gehört ein natives Listenelement dorthin, unabhängig vom visuellen Styling.
<!-- WRONG: div soup, no semantic list -->
<div class="flex flex-col gap-2">
<div class="border-b py-2">Versandkostenfrei ab 50 EUR</div>
<div class="border-b py-2">2 Jahre Garantie</div>
<div class="border-b py-2">30 Tage Rückgaberecht</div>
</div>
<!-- RIGHT: semantic list with identical visual result -->
<ul class="flex flex-col gap-2 list-none m-0 p-0">
<li class="border-b py-2">Versandkostenfrei ab 50 EUR</li>
<li class="border-b py-2">2 Jahre Garantie</li>
<li class="border-b py-2">30 Tage Rückgaberecht</li>
</ul>
4. Navigation, Warenkorb und Produktraster als Listen
Navigationsmenüs, Warenkorb-Positionen und Produktraster sind die drei häufigsten Stellen in Magento- und Hyvä-Shops, an denen echte Listen fälschlicherweise als div-Konstrukte umgesetzt werden. Eine Hauptnavigation gehört immer in <nav><ul><li><a>, damit ein Screenreader-Nutzer per Tastenkürzel direkt zur Liste der Navigationspunkte springen und deren Anzahl erfassen kann. Bei Warenkorb-Positionen signalisiert eine <ul>, dass mehrere gleichartige Einheiten vorliegen, was bei aria-live-Ankündigungen nach dem Hinzufügen eines Artikels ebenfalls präziser funktioniert.
Produktraster (Product Grids) sind ein Grenzfall: Eine Sammlung von Produktkarten ist inhaltlich eine Liste, wird aber oft mit CSS Grid statt Flexbox realisiert. Das ist kein Widerspruch, denn display: grid lässt sich problemlos auf <ul> und <li> anwenden, ohne dass CSS die zugrunde liegende Semantik verändert. In Hyvä-Themes mit Tailwind CSS bedeutet das konkret: Die Grid-Klassen wandern auf das <ul>-Element, während jede Produktkarte in einem <li> steckt, das selbst wieder list-none erhält, um die Browser-Standardaufzählungszeichen zu entfernen, ohne die Semantik zu verlieren.
<!-- Hyvä phtml: product grid as semantic list with CSS grid -->
<nav aria-label="{{ __('Hauptnavigation') }}">
<ul class="flex gap-6 list-none m-0 p-0">
<li><a href="{{ $categoryUrl }}" class="hover:underline">Uhren</a></li>
<li><a href="{{ $categoryUrl2 }}" class="hover:underline">Schmuck</a></li>
</ul>
</nav>
<ul class="grid grid-cols-2 sm:grid-cols-4 gap-6 list-none m-0 p-0" aria-label="{{ __('Produkte') }}">
<?php foreach ($products as $product): ?>
<li>
<a href="{{ $product->getProductUrl() }}" class="block">
<img src="{{ $product->getImageUrl() }}" alt="{{ $escaper->escapeHtmlAttr($product->getName()) }}" width="300" height="300">
<span class="block mt-2 font-semibold"><?= $escaper->escapeHtml($product->getName()) ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
5. Tabellen-Grundlagen: th, scope und caption
Eine <table> ist für echte tabellarische Daten gedacht: Zeilen und Spalten, die in einer zweidimensionalen Beziehung zueinander stehen, etwa eine Preisliste mit Größen in den Spalten und Produkten in den Zeilen. Das Attribut scope="col" auf einem <th>-Element erklärt, dass diese Zelle als Überschrift für die gesamte Spalte gilt, während scope="row" dasselbe für eine Zeile signalisiert. Ohne scope muss ein Screenreader raten, welche Überschrift zu welcher Datenzelle gehört, was bei mehr als zwei Spalten schnell zu Fehlzuordnungen führt.
Das <caption>-Element, direkt nach dem öffnenden <table>-Tag platziert, liefert einen kurzen, beschreibenden Titel, der von Screenreadern automatisch vorgelesen wird, bevor der Nutzer in die eigentlichen Tabellendaten eintaucht. Das ersetzt Konstruktionen mit einer separaten Überschrift außerhalb der Tabelle, die programmatisch nicht mit der Tabelle verknüpft ist. Zusammen mit <thead>, <tbody> und korrekt gesetztem scope entsteht eine Tabelle, die ein Screenreader Zelle für Zelle mit vollständigem Kontext vorlesen kann, ohne dass der Nutzer selbst mitzählen muss, in welcher Spalte er sich befindet.
<!-- Simple data table with caption and scope -->
<table class="w-full text-sm border-collapse">
<caption class="text-left font-semibold mb-2">Verfügbare Größen und Preise</caption>
<thead>
<tr>
<th scope="col">Größe</th>
<th scope="col">Farbe</th>
<th scope="col">Preis</th>
<th scope="col">Verfügbar</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">S</th>
<td>Schwarz</td>
<td>49,90 EUR</td>
<td>Ja</td>
</tr>
<tr>
<th scope="row">M</th>
<td>Schwarz</td>
<td>49,90 EUR</td>
<td>Nein</td>
</tr>
</tbody>
</table>
6. Komplexe Tabellen: headers, id und mehrstufige Header
Sobald eine Tabelle mehrstufige Überschriften hat, etwa eine gruppierte Kopfzeile mit „Q1" und „Q2", die sich jeweils in „Umsatz" und „Stückzahl" aufteilt, reicht scope allein nicht mehr aus, weil eine Datenzelle dann zu mehr als einer Überschrift gleichzeitig gehört. Für diese Fälle definiert HTML das headers-Attribut: Jede <th>-Zelle bekommt eine eindeutige id, und jede <td>-Zelle referenziert per headers="id1 id2" alle zutreffenden Überschriften, getrennt durch Leerzeichen. Screenreader lesen dann bei jeder Datenzelle die vollständige Kette aller zugehörigen Überschriften vor, bevor der eigentliche Wert folgt.
In der Praxis lohnt sich diese Komplexität fast nie für Standard-Shopdaten. Bevor eine komplexe verschachtelte Tabelle gebaut wird, lohnt sich die Frage, ob sich die Daten nicht in zwei einfachere, klar getrennte Tabellen aufteilen lassen. Einfache Tabellen mit scope="col" und scope="row" sind für Screenreader-Nutzer erheblich leichter zu erfassen als eine Tabelle mit drei Ebenen verschachtelter Header, selbst wenn headers technisch korrekt gesetzt ist. Komplexität in der Datenstruktur sollte, wo möglich, in der Präsentation reduziert werden, nicht nur technisch korrekt ausgezeichnet.
<!-- Multi-level table: headers attribute for unambiguous mapping -->
<table class="w-full text-sm border-collapse">
<caption class="text-left font-semibold mb-2">Umsatz und Stückzahl nach Quartal</caption>
<thead>
<tr>
<th id="produkt" rowspan="2" scope="col">Produkt</th>
<th id="q1" colspan="2" scope="colgroup">Q1</th>
<th id="q2" colspan="2" scope="colgroup">Q2</th>
</tr>
<tr>
<th id="q1-umsatz" headers="q1" scope="col">Umsatz</th>
<th id="q1-stueck" headers="q1" scope="col">Stückzahl</th>
<th id="q2-umsatz" headers="q2" scope="col">Umsatz</th>
<th id="q2-stueck" headers="q2" scope="col">Stückzahl</th>
</tr>
</thead>
<tbody>
<tr>
<th id="uhr-a" scope="row">Uhr A</th>
<td headers="uhr-a q1 q1-umsatz">12.400 EUR</td>
<td headers="uhr-a q1 q1-stueck">248</td>
<td headers="uhr-a q2 q2-umsatz">15.900 EUR</td>
<td headers="uhr-a q2 q2-stueck">318</td>
</tr>
</tbody>
</table>
7. Warum Tabellen niemals für visuelles Layout dienen sollten
In den frühen 2000er-Jahren war die Layout-Tabelle Standardpraxis, um mehrspaltige Webseiten zu bauen, bevor CSS Grid und Flexbox verfügbar waren. Dieses Muster ist heute nicht nur technisch überholt, sondern aktiv schädlich für die Zugänglichkeit: Ein Screenreader interpretiert jede <table> als tabellarische Daten und kündigt Zeilen- und Spaltenzahlen an, selbst wenn die Tabelle nur zur visuellen Anordnung von Logo, Navigation und Inhalt dient. Der Nutzer hört dann sinnlose Ansagen wie „Tabelle mit 3 Spalten und 1 Zeile", die keinerlei inhaltlichen Mehrwert bieten und die Navigation aktiv erschweren.
Der einzige zulässige Ausweg, wenn aus Legacy-Gründen dennoch eine Tabelle für Layout verwendet werden muss, ist role="presentation" auf dem <table>-Element, was die tabellarische Semantik für die Accessibility-API vollständig entfernt. Diese Lösung ist aber ein Notbehelf und keine Empfehlung, denn moderne CSS-Layout-Mechanismen wie Grid und Flexbox lösen dasselbe visuelle Problem ohne semantische Nebenwirkungen. Die klare Regel lautet: <table> ausschließlich für Daten, die tatsächlich in einer Zeilen-Spalten-Beziehung stehen, niemals für die Anordnung von Seitenelementen.
8. Screenreader-Test: wie NVDA und VoiceOver Listen und Tabellen vorlesen
Der zuverlässigste Weg, semantische Auszeichnung zu prüfen, ist der direkte Test mit einem Screenreader, weil automatisierte Linter wie axe-core nur einen Teil der möglichen Fehler erkennen. Mit NVDA (Windows, kostenlos) navigiert man per L-Taste durch alle Überschriften und per Insert+F7 durch eine Elementliste, die alle Listen und Tabellen einer Seite mit ihrer jeweiligen Größe anzeigt. Eine korrekt ausgezeichnete <ul> mit fünf Einträgen wird dabei explizit als „Liste mit 5 Einträgen" geführt, eine div-Suppe erscheint dort gar nicht erst.
VoiceOver auf macOS und iOS bietet mit dem Rotor (Ctrl+Option+U) eine vergleichbare Übersicht über Tabellen und Listen. Bei Tabellen navigiert VoiceOver mit Ctrl+Option+Pfeiltasten zellenweise und liest bei jedem Wechsel automatisch die zugehörige Spalten- und Zeilenüberschrift vor, vorausgesetzt scope ist korrekt gesetzt. Ein praktischer Testablauf für jede neue Komponente: Tab-Navigation ohne Maus durchführen, Elementliste prüfen, und bei Tabellen gezielt in die mittleren Zellen springen, um zu verifizieren, dass die Überschriften-Ansage stimmt und nicht nur bei der ersten Spalte zufällig korrekt klingt.
Als Ergänzung zum manuellen Screenreader-Test lässt sich axe-core in die CI-Pipeline integrieren und gezielt auf die Regeln th-has-data-cells, td-headers-attr, list und listitem beschränken. Solche automatisierten Checks erkennen fehlende th- und scope-Zuordnungen zuverlässig, ersetzen aber, wie im vorherigen Abschnitt beschrieben, niemals den manuellen Test mit einem echten Screenreader.
9. Praxisbeispiel: barrierefreie Produktvergleichstabelle
Produktvergleichstabellen sind ein häufiges E-Commerce-Feature und gleichzeitig ein Musterbeispiel für echte tabellarische Daten: Mehrere Produkte in Spalten, mehrere Merkmale in Zeilen, klare Zeilen- und Spaltenbeziehungen. Die Umsetzung mit <caption>, <th scope="col"> für die Produktnamen in der Kopfzeile und <th scope="row"> für die Merkmalsbezeichnung in der ersten Spalte jeder Zeile erfüllt WCAG 1.3.1 vollständig, ohne dass zusätzliche ARIA-Attribute nötig sind. Native HTML-Semantik ist hier bewusst die erste Wahl vor ARIA, denn die „No ARIA is better than Bad ARIA"-Regel gilt auch für Tabellen.
Wichtig bei der visuellen Gestaltung mit Tailwind CSS ist, dass Zebra-Streifen und Farbcodierung wie „grün = verfügbar" niemals die einzige Information tragen dürfen. Ein Screenreader liest keine Hintergrundfarbe vor, weshalb zusätzlicher Text wie „Verfügbar" oder ein <span class="sr-only"> mit dem Status nötig ist. Die folgende Tabelle zeigt eine vollständig barrierefreie Umsetzung inklusive Falsch-Richtig-Vergleich der häufigsten Fehler bei Listen und Tabellen in der Praxis.
| Anwendungsfall | Unzugänglich | Barrierefrei | Warum wichtig |
|---|---|---|---|
| Merkmalsliste | div.flex.flex-col |
ul>li |
Anzahl der Einträge wird angesagt |
| Checkout-Schritte | div mit Nummern als Text |
ol>li |
Position "3 von 5" automatisch |
| Produktattribute | div mit Doppelpunkt-Text |
dl>dt/dd |
Schlüssel-Wert-Beziehung erkennbar |
| Vergleichstabelle | td statt th in Kopfzeile |
th scope="col" |
Spaltenbezug jeder Zelle klar |
| Verfügbarkeit | nur grüner Punkt per CSS | Text + sr-only-Status |
Farbe allein transportiert nichts |
| Seitenlayout | table für Spaltenanordnung |
CSS Grid / Flexbox | Keine sinnlose Tabellenansage |
Mironsoft
Barrierefreiheit, semantisches HTML und WCAG-Audits für Magento-Shops
Listen und Tabellen wirklich barrierefrei umsetzen?
Wir prüfen bestehende Magento- und Hyvä-Templates auf div-Suppe, fehlende scope-Attribute und Layout-Tabellen und ersetzen sie durch semantisch korrekte, WCAG-konforme Auszeichnung, ohne visuelles Design zu verändern.
Accessibility-Audit
Manuelle Screenreader-Prüfung plus axe-core-Analyse aller Templates
Template-Refactoring
div-Suppe durch ul/ol/dl ersetzen, th und scope in Tabellen nachrüsten
WCAG-Schulung
Praxis-Workshop für euer Team zu semantischem HTML und ARIA
10. Zusammenfassung
Die semantische Auszeichnung von Listen und Tabellen entscheidet direkt darüber, ob Inhalte für Screenreader-Nutzer nutzbar sind. <ul>, <ol> und <dl> ersetzen div-Konstrukte für Aufzählungen, Reihenfolgen und Schlüssel-Wert-Paare und liefern automatisch Positions- und Mengenangaben, die assistive Technologien vorlesen. <th scope="col">, <th scope="row"> und <caption> machen echte Datentabellen navigierbar, während das headers-Attribut für mehrstufige Header nötig wird, wenn scope allein nicht mehr ausreicht.
Tabellen für visuelles Layout sind seit der Einführung von CSS Grid und Flexbox obsolet und erzeugen bei jedem Seitenaufruf sinnlose Screenreader-Ansagen. Der zuverlässigste Weg, semantische Fehler zu finden, bleibt der manuelle Test mit NVDA oder VoiceOver, ergänzt durch automatisierte axe-core-Checks in der CI-Pipeline. Wer diese Grundregeln konsequent in Magento- und Hyvä-Templates anwendet, macht Produktlisten, Navigationsmenüs und Vergleichstabellen für alle Nutzergruppen gleichermaßen zugänglich.
Listen und Tabellen barrierefrei auszeichnen: Das Wichtigste auf einen Blick
Listentyp wählen
ul ohne Reihenfolge, ol mit Reihenfolge, dl für Schlüssel-Wert-Paare. Nie div-Suppe für echte Aufzählungen.
th mit scope
scope="col" und scope="row" auf jedem Header ordnen Datenzellen eindeutig zu.
caption statt Überschrift
caption im table-Element wird von Screenreadern automatisch vorgelesen, bevor Daten folgen.
Kein Layout mit table
CSS Grid und Flexbox statt Layout-Tabellen. Screenreader kündigen sonst sinnlose Zeilen/Spalten an.