Responsiv und Barrierefrei: Eine Navigation, die für alle funktioniert
Einführung
Für eine Navigation, die sich automatisch der Bildschirmgröße anpasst, gibt es viele Ansätze, die sich schnell umsetzen lassen. In diesem Tutorial bauen wir eine Navigation von Anfang an responsiv und barrierefrei. Das bedeutet, dass sie nicht nur auf Smartphones und Tablets funktioniert, sondern auch für Menschen mit Behinderungen zugänglich ist.
Das ist wichtig, weil wir allen Nutzern eine optimale Benutzererfahrung bieten möchten. Aber auch der Gesetzgeber verpflichtet Unternehmen mit dem BFSG, ihre digitalen Angebote barrierefrei zu gestalten.
Voraussetzung: In diesem Tutorial gehen wir davon aus, dass du das Starterkit nutzt und als Template den Multi-Pager gewählt hast. Dies ist bereits für unsere Zwecke vorkonfiguriert.
Unser Lernweg in diesem Tutorial
Wir arbeiten bewusst in zwei Schritten:
- Basis-Version verstehen: Du baust eine einfache, gut zugängliche Navigation mit einem sichtbaren Button und einem responsiven Layout.
- Progressive Enhancement1 anwenden: Danach wird der Button per
templateund JavaScript eingefügt, damit die Navigation schrittweise robuster wird.
Das Ziel: Wir erarbeiten uns eine Navigation, die auf mobilen Endgeräten als Overlay von rechts ins Bild fährt, auf Desktop aber direkt sichtbar ist. Und das alles mit sauberem HTML, CSS und JavaScript.
Die Basis
Jede gute Website beginnt mit sauberem, semantischem HTML. Das bedeutet, wir verwenden HTML-Elemente für den Zweck, für den sie gedacht sind. Für unsere Navigation benötigen wir einen Header, ein Navigationselement und eine Liste mit Links.
HTML-Grundgerüst
Den Header gibt es im Starterkit bereits, dieser ist als <include />-Tag in den HTML-Dateien eingebunden. Er dient als Platzhalter für den Header aus der src/snippets/header.html. Öffne diese Datei und ersetze den Inhalt mit folgendem Code:
<header class="main-header"> <a href="/" class="website-title"> Barrierefreie Navigation </a>
<nav id="main-nav" aria-label="Hauptnavigation"> <ul id="main-nav-list"> <li> <a href="/">Startseite</a> </li> <li> <a href="/fotos/">Fotos</a> </li> </ul> </nav></header>Diese Struktur ist einfach, aber effektiv. Sie enthält:
<header>: Dies ist der Kopfbereich unserer Seite. Er enthält typischerweise das Logo, den Titel unserer Website und die Hauptnavigation.<a>: Ein Link zur Startseite. In der Regel erwarten Nutzer, dass sie durch Klicken auf das Logo oder den Titel zur Startseite gelangen.<nav>: Dieses Element signalisiert, dass hier die Navigation zu finden ist. Dasaria-labelist eine wichtige Ergänzung für die Zugänglichkeit, da es Screenreadern genau sagt, um welche Art von Navigation es sich handelt.<ul>: Eine ungeordnete Liste ist die semantisch korrekte Art, eine Gruppe von Navigationslinks darzustellen. Die Reihenfolge ist dabei nicht relevant.
Im Moment sieht das noch unspektakulär aus. Das ändern wir jetzt mit CSS. Wir beginnen, wie sich das gehört, mit der Gestaltung für mobile Endgeräte.
Styles für mobile Endgeräte
Der Kopf der Website <header> soll in unserem Szenario beim Scrollen am oberen Rand des Bildschirms bleiben, während die Navigation als Overlay von rechts ins Bild fährt, wenn man auf einen Menü-Button tippt. Den Button gibt es noch nicht, aber wir bereiten das Styling schon mal vor. Ergänze die folgenden Regeln in den genannten CSS-Dateien:
:root { --header-size: 5rem; --sp-content: 1.5rem;}* { box-sizing: border-box;}
body { /* Platzhalter, damit der Inhalt nicht vom fixierten Header verdeckt wird */ padding-block-start: var(--header-size);}.main-header { /* Header bleibt am oberen Rand fixiert */ position: fixed; inset: 0 0 auto 0; z-index: 20;
display: flex; align-items: center; justify-content: space-between; block-size: var(--header-size); /* Höhe des Headers */ padding-inline: var(--sp-content); border-block-end: 1px solid var(--clr-line); background-color: rgba(255, 255, 255, 0.8); backdrop-filter: blur(3px); /* Milchglas-Effekt */}
/* Die Navigationsliste als Overlay (Mobile) */#main-nav-list { /* Positionierung am rechten oberen Rand */ position: fixed; inset-block-start: 0; inset-inline-end: 0; z-index: 20;
block-size: 100dvh; /* Nimmt die volle Bildschirmhöhe ein */ inline-size: min(20rem, 80vw); /* Breite des Menüs */
background-color: var(--clr-bg); box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
/* Menü initial ausserhalb des Bildschirms verstecken */ transform: translateX(100%); transition: transform 300ms ease-in-out; /* Sanfte Animation */
list-style: none; margin: 0; padding: var(--header-size) var(--sp-content) var(--sp-content);
/* Styling für die Links */ a { display: block; color: currentColor; /* Vererbt die Textfarbe */ padding-block: 0.85em; text-decoration: none; }}Was passiert hier?
- Im
:root-Bereich definieren wir zentrale Werte wie Größen und Abstände an einer Stelle. Diese werden später mitvar(--name)referenziert. Das macht spätere Änderungen deutlich einfacher. box-sizing: border-box: Stellt sicher, dass Padding und Border in der Breite und Höhe der Elemente enthalten sind. Das vereinfacht das Layout.- Fixierter Header: Mit
position: fixedbleibt der Header auch beim Scrollen immer sichtbar.padding-block-startambodyverhindert, dass der Seiteninhalt dahinter verschwindet. - Navigation als Overlay: Die Liste
#main-nav-listwird mitposition: fixedam rechten oberen Rand positioniert und nimmt die volle Höhe des Bildschirms ein. Der entscheidende Trick isttransform: translateX(100%). Damit schieben wir das Element um 100% seiner eigenen Breite nach rechts — es ist also ausserhalb des sichtbaren Bereichs.
Achtung: Aktuell bleibt der Header bereits beim Scrollen an seiner Position, die Navigation ist allerdings durch
transform: translateX(100%)noch verborgen. Das ist genau das, was wir wollen. Später soll die Navigation mit einer Animation ins Bild fahren, wenn der Benutzer auf den Menü-Button klickt.
Interaktivität mit JavaScript
Mit HTML haben wir die Navigation strukturiert und mit CSS grob gestylt. Jetzt kommt JavaScript dazu. JavaScript ist die Programmiersprache im Browser, mit der wir auf Klicks reagieren und Elemente verändern können. In unserem Fall steuert JavaScript den Button, damit sich das Menü öffnen und wieder schließen lässt.
In der Basis-Version verwenden wir einen sichtbaren Button direkt im HTML. Das ist für den Einstieg einfacher zu verstehen, weil du den Zustand des Menüs direkt im Markup sehen kannst.
Ergänze dafür den Button in der Navigation:
<header class="main-header"> <a href="/" class="website-title"> Barrierefreie Navigation </a>
<nav id="main-nav" aria-label="Hauptnavigation"> <button type="button" class="nav-toggle" aria-expanded="false" aria-controls="main-nav-list"> Menü </button>
<ul id="main-nav-list"> <li> <a href="/">Startseite</a> </li> <li> <a href="/fotos/">Fotos</a> </li> </ul> </nav></header>Die beiden wichtigsten Attribute sind:
aria-expanded="false": Dieses Attribut beschreibt, ob das Menü gerade geöffnet oder geschlossen ist. Der Wertfalsebedeutet: Das Menü ist aktuell geschlossen.aria-controls="main-nav-list": Der Button steuert die Liste mit dieser ID.
Entferne nun den Demo-Modus in deiner src/main.js und ersetze ihn durch den folgenden Code:
const navButton = document.querySelector('#main-nav .nav-toggle');
if (navButton) { navButton.addEventListener('click', () => { const isExpanded = navButton.getAttribute('aria-expanded') === 'true'; navButton.setAttribute('aria-expanded', String(!isExpanded)); navButton.textContent = isExpanded ? 'Menü' : 'Schließen'; });}Der Button dient gleichzeitig als Öffnen- und Schließen-Button: Ein Klick öffnet das Menü und ändert den Text auf „Schließen”, ein weiterer Klick schließt es wieder. Ein separater Schließen-Button ist nicht nötig.
Werfen wir mal einen Blick in die Developer Tools2 des Browsers, damit du sehen kannst, wie sich das aria-expanded-Attribut ändert, wenn du auf den Button klickst. Das ist wichtig, um zu verstehen, wie die Interaktion funktioniert.
Jetzt fehlt noch die CSS-Regel, die das Menü sichtbar macht. Sobald aria-expanded den Wert true hat, soll die Navigation ins Bild fahren.
#main-nav .nav-toggle { all: unset; position: relative; /* Stacking Context für z-index */ z-index: 21; /* Bleibt über dem Overlay (#main-nav-list hat z-index: 20) */ display: grid; place-content: center; cursor: pointer; inline-size: var(--header-size); block-size: var(--header-size);
/* all: unset entfernt auch den Fokusring – für Tastaturnutzer wieder herstellen */ &:focus-visible { outline: 4px solid currentColor; outline-offset: 2px; }
&[aria-expanded='true'] + #main-nav-list { transform: translateX(0); }}Die mobile Version funktioniert, prima. Jetzt kümmern wir uns um den Desktop.
Responsive Anpassung für größere Bildschirme
Auf größeren Bildschirmen ist ein verstecktes Menü hinter einem Button keine gute Lösung – nicht weil es technisch falsch wäre, sondern weil es aus UX-Sicht besser ist, die Links direkt anzuzeigen: mehr Überblick, weniger Klicks, keine versteckten Informationen.
Dafür nutzen wir je eine Media Query am Ende eines bestehenden Blocks, den wir für die mobile Version geschrieben haben. So können wir die Regeln für größere Bildschirme ergänzen, ohne die Mobile-Styles zu beeinflussen.
#main-nav-list { /* Hier stehen die Mobile-Styles, nicht entfernen! */
@media (width >= 48em) { position: static; block-size: auto; inline-size: auto;
background-color: transparent; box-shadow: none; padding: 0;
transform: translateX(0);
display: flex; flex-direction: row; gap: 2ch; }}
#main-nav .nav-toggle { /* Hier stehen die Mobile-Styles, nicht entfernen! */
@media (width >= 48em) { display: none; }}Was haben wir geändert?
- Mit
@media (width >= 48em) { … }definieren wir einen Block, dessen Regeln nur dann angewendet werden, wenn die Breite des Browserfensters größer ist als 48em (ca. 768px). - Der Button wird mit
.nav-toggle { display: none; }ausgeblendet. - Schlussendlich passen wir die Liste
ulselbst an und überschreiben damit die Mobile-Styles. Die Liste ist jetzt statisch positioniert, also ein normaler Teil des Headers. Mitdisplay: flexundflex-direction: rowordnen wir die Links nebeneinander an.transform: translateX(0)stellt sicher, dass die Liste sichtbar ist, falls das JavaScript sie (auf Mobile) ausgeblendet hatte.
Der finale Schliff
Da die Basis-Version funktioniert, verbessern wir sie jetzt mit Progressive Enhancement. In der aktuellen Implementierung hat der Menü-Button keine Funktion, wenn JavaScript deaktiviert ist. Gleichzeitig verstecken wir auf kleineren Bildschirmen die Navigation, sie ist also gar nicht mehr zugänglich.
Das Ziel: Der Button wird nur dann erzeugt, wenn JavaScript verfügbar ist. So bleibt dein HTML schlank, und die Navigation funktioniert auch dann noch, wenn JavaScript nicht verfügbar ist.
HTML auf template umstellen
Anstelle eines sichtbaren Buttons im HTML verwenden wir nun ein template. Ein Template ist ein unsichtbares Element, das HTML enthält, das erst mit JavaScript in die Seite eingefügt wird. Lösche den Button aus dem HTML und ergänze stattdessen das Template:
<nav id="main-nav" aria-label="Hauptnavigation"> <button type="button" class="nav-toggle" aria-expanded="false" aria-controls="main-nav-list"> Menü </button>
<ul id="main-nav-list"> <li> <a href="/">Startseite</a> </li> <li> <a href="/fotos/">Fotos</a> </li> </ul>
<template id="main-nav-button"> <button type="button" class="nav-toggle" aria-label="Menü" aria-expanded="false" aria-controls="main-nav-list"> <svg viewBox="0 0 24 24" aria-hidden="true" class="icon"> <path d="M4 6h16" class="line top" /> <path d="M4 12h16" class="line middle" /> <path d="M4 18h16" class="line bottom" /> </svg> </button> </template></nav>JavaScript für template + Tastatur erweitern
Jetzt passen wir das JavaScript an, damit es den Button aus dem Template klont und in die Navigation einfügt. Gleichzeitig erweitern wir die Funktionalität, damit man das Menü auch mit der Escape-Taste schließen kann und die Links im Menü das Öffnen rückgängig machen.
Entferne den bisherigen JavaScript-Code für die Navigation und ersetze ihn durch den folgenden:
const nav = document.querySelector('#main-nav');const navList = document.querySelector('#main-nav-list');const navButtonTpl = document.querySelector('#main-nav-button');
if (nav && navList && navButtonTpl) { const navButtonClone = navButtonTpl.content.cloneNode(true); const navButton = navButtonClone.querySelector('button'); const navIcon = navButtonClone.querySelector('.icon');
nav.insertBefore(navButtonClone, navList);
navButton.addEventListener('click', () => { const isExpanded = navButton.getAttribute('aria-expanded') === 'true'; navButton.setAttribute('aria-expanded', String(!isExpanded)); navIcon.classList.toggle('open', !isExpanded); });}CSS für robusten Zustand ergänzen
Im letzten Schritt ergänzen wir das bestehende CSS um folgende Dinge:
- Zustandslogik, damit die Liste nur bei vorhandenem Toggle-Button versteckt wird
- Styling und Animation für das SVG-Icon im Button
Wichtig für Progressive Enhancement: Ohne JavaScript gibt es keinen Toggle-Button. Dann bleibt die Navigation sichtbar und nutzbar.
#main-nav-list { /* Bisherige Styles ändern… */ transform: translateX(100%); transform: translateX(0);}
#main-nav .nav-toggle { /* Bisherige Styles ergänzen… */ + #main-nav-list { transform: translateX(100%); }}
/* Hamburger-Menü + Icon-Animation */#main-nav .icon { inline-size: 24px; block-size: 24px;
path { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: 200ms; }
&.open { .top { transform: rotate(45deg) translate(5px, -6px); transform-origin: 0 0; }
.middle { opacity: 0; }
.bottom { transform: rotate(-45deg) translate(0, -6px); transform-origin: 50% 50%; } }}Du hast jetzt bereits eine solide Navigation, die auf verschiedenen Bildschirmgrößen funktioniert und per aria-expanded gesteuert wird. Das ist ein guter Ausgangspunkt, um sie weiter zu verfeinern.
Ausgereiftere Variante
Folgende Dinge könntest du noch in Betracht ziehen, um deine Navigation noch besser zu machen:
- Tastatur-Navigation
- Man kann das Menü mit der Escape-Taste schliessen.
- Unbeabsichtigtes Scrollen verhindern
- Bei geöffnetem Menü scrollt neben der Navigation auch die dahinterliegende Seite. Das kannst du mit der Pseudoklasse
:has(…)verhindern. - Semantik für den aktiven Link
- Über
aria-current="page"wird die aktuell aktive Seite für Screenreader eindeutig markiert.
Ich habe für dich ein vollständiges Beispiel mit all diesen Verbesserungen als CodePen zusammengestellt. Schaue es dir gern an, um zu sehen, wie die einzelnen Teile zusammenwirken.
Öffne den Codepen.
Fußnoten
-
Progressive Enhancement ist eine Entwicklungsstrategie, bei der du zuerst eine funktionierende Grundlage mit reinem HTML baust und dann CSS und JavaScript als Erweiterung draufsetzt. Das Prinzip: Was mit HTML funktioniert, funktioniert immer — auch wenn CSS oder JavaScript nicht geladen werden oder deaktiviert sind. ↩
-
Developer Tools sind in jedem modernen Browser eingebaut und lassen sich per Rechtsklick → „Untersuchen” öffnen. Du kannst damit den HTML-Baum inspizieren, CSS-Regeln live ändern und JavaScript-Fehler aufspüren — unverzichtbar für die tägliche Arbeit als Webentwickler:in. ↩