Wer nach Lösungen sucht, Tabs in Websiten darzustellen, findet hauptsächlich Plugins, die das ganze mit Javascript abbilden. Das geht durchaus auch barrierefrei – aber nicht ohne Javascript. Dazu müssen die Elemente mit role="tablist" und role="tab" für ScreenreaderScreenreader Software, die Texte am Bildschirm laut vorliest oder als Braille ausgibt. extra definiert werden. Das geht doch sicherlich eleganter?
Eine verbreitete Methode für umschaltende Navigationen ohne Javascript sind versteckte Radio-Buttons – das verwende auch ich gern und oft und auch für dieses Praxisbeispiel. Die sichtbaren Tabs sind Labels für (unsichtbare) Radiobuttons, und im CSS ist definiert, was bei welchem aktiven Radio-Button sichtbar sein soll.
<div class="nurtabs">
<ul class="tabs">
<li><label><input type="radio" name="nurtabs" checked="">Tab 1</label></li>
<li><label><input type="radio" name="nurtabs">Tab 2</label></li>
<li><label><input type="radio" name="nurtabs">Tab 3</label></li>
</ul>
<div>
<p>Inhalt 1</p>
</div>
<div>
<p>Inhalt 2</p>
</div>
<div>
<p>Inhalt 3</p>
</div>
</div>
div.tabs summary {
cursor: pointer;
}
div.tabs details.open > summary {
background: #ccc;
}
@media screen and (max-width: 1023.98px) {
div.tabs ul.tabs {
display: none;
}
}
@media screen and (min-width: 1024px) {
div.tabs ul.tabs input[type=radio] {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
div.tabs > details::details-content {
content-visibility: hidden;
block-size: 0;
}
div.tabs:has(ul.tabs li:nth-child(1) input:checked) > details:nth-of-type(1)::details-content,
div.tabs:has(ul.tabs li:nth-child(2) input:checked) > details:nth-of-type(2)::details-content,
div.tabs:has(ul.tabs li:nth-child(3) input:checked) > details:nth-of-type(3)::details-content {
content-visibility: visible;
block-size: auto;
}
div.tabs ul.tabs {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 0;
padding: 0;
}
div.tabs ul.tabs > li {
list-style-type: none;
margin: 0;
}
div.tabs ul.tabs li:has(input:checked) label {
background-color: #ccc;
}
div.tabs details {
margin-top: 0;
}
div.tabs details summary {
display: none;
}
div.tabs ul.tabs li label {
display: inline-block;
padding: 10px 20px;
white-space: nowrap;
cursor: pointer;
}
}
Kein JS notwendig
Doch wie stellt man Tabs überhaupt sinnvoll mobil dar? Sobald es mehr werden, als horizontal nebeneinander passen (oder die Tab-Beschriftungen länger werden), müssen sie untereinander angeordnet werden – was eigentlich einem Akkordeon entspricht. Und dafür bietet HTML das native und barrierefreie <details> an.
HTML
<div class="nurdetails">
<details class="tab1" open=""><summary>Summary 1</summary><div>
<p>Details 1</p>
</div></details>
<details class="tab2"><summary>Summary 2</summary><div>
<p>Details 2</p>
</div></details>
<details class="tab3"><summary>Summary 3</summary><div>
<p>Details 3</p>
</div></details>
</div>
CSS
Kein CSS notwendig
JS
Kein JS notwendig
Also warum nicht beides kombinieren? Wir müssen keine künstlichen Aria-RollenARIA-Rollen Zusätzliche Infos im HTML, die Screenreadern sagen, was ein Element ist (z. B. „Button“ oder „Navigation“). festlegen, keine Styles für mobile Tabs entwickeln, stattdessen fügen wir nur Radio-Buttons mit Labels für den Desktop hinzu, die das folgende <details>-Akkordeon steuern.
HTML
<div class="tabs" role="group" aria-label="Tabs">
<ul class="tabs">
<li><label><input type="radio" name="tabs" checked="">Tab 1</label></li>
<li><label><input type="radio" name="tabs">Tab 2</label></li>
<li><label><input type="radio" name="tabs">Tab 3</label></li>
</ul>
<details class="open" open=""><summary>Summary 1</summary><div>
<p>Details 1</p>
</div></details>
<details><summary>Summary 2</summary><div>
<p>Details 2</p>
</div></details>
<details><summary>Summary 3</summary><div>
<p>Details 3</p>
</div></details>
</div>
CSS
div.tabs summary {
cursor: pointer;
}
div.tabs details.open > summary {
background: #ccc;
}
@media screen and (max-width: 1023.98px) {
div.tabs ul.tabs {
display: none;
}
}
@media screen and (min-width: 1024px) {
div.tabs ul.tabs input[type=radio] {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
div.tabs > details::details-content {
content-visibility: hidden;
block-size: 0;
}
div.tabs:has(ul.tabs li:nth-child(1) input:checked) > details:nth-of-type(1)::details-content,
div.tabs:has(ul.tabs li:nth-child(2) input:checked) > details:nth-of-type(2)::details-content,
div.tabs:has(ul.tabs li:nth-child(3) input:checked) > details:nth-of-type(3)::details-content {
content-visibility: visible;
block-size: auto;
}
div.tabs ul.tabs {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 0;
padding: 0;
}
div.tabs ul.tabs > li {
list-style-type: none;
margin: 0;
}
div.tabs ul.tabs li:has(input:checked) label {
background-color: #ccc;
}
div.tabs details {
margin-top: 0;
}
div.tabs details summary {
display: none;
}
div.tabs ul.tabs li label {
display: inline-block;
padding: 10px 20px;
white-space: nowrap;
cursor: pointer;
}
}
JS
(function () {
"use strict";
document.addEventListener("DOMContentLoaded", function () {
var radios = document.querySelectorAll("div.tabs input[type=radio]");
var panels = document.querySelectorAll("div.tabs > details");
radios.forEach(function (radio, i) {
radio.addEventListener("change", function () {
panels.forEach(function (panel, j) {
panel.open = (i === j);
panel.classList.toggle("open", i === j);
});
});
});
panels.forEach(function (panel, i) {
panel.querySelector("summary").addEventListener("click", function () {
setTimeout(function () {
panel.classList.toggle("open", panel.open);
if (panel.open) radios[i].checked = true;
}, 0);
});
});
});
})();
Letztendlich habe ich doch etwas JS hinzugefügt. Aber: Das ist für die Funktionalität in aktuellen Browsern nicht notwendig, man kann es auch weglassen. ::details-content und :has() reichen aus, um Tab-Leiste und Akkordeon synchron zu halten. Nur Safari-Versionen von vor 2025 kennen beides noch nicht, dort sorgt das Script dafür, beim Umschalten zwischen Desktop- und Mobilansicht (bei Änderung der Browsergröße am Desktop) die geöffneten Tabs bzw. Details zu synchronisieren. Zusätzlich wird eine CSS-Klasse gesetzt, um <details> im alten Safari stylen zu können..
Laut Caniuse betrifft das zwar nur rund 0,5 % aller Aufrufe, aber Mac-User hängen oft auf veralteten Versionen fest, da die Safari-Version an die macOS-Version gekoppelt ist, die sich wiederum nicht auf jedem älteren Rechner aktualisieren lässt. Und wenn auf Kunden-Seite so jemand sitzt, ist ihm egal, wie viele Menschen das betrifft, es sieht blöd aus.
Ergebnis: Eine responsive, automatisch barrierefreie Lösung ohne Javascript (für 99,5% der Besucher).