Abschnitt 20 · Lektion 4
Barrierefreiheit mit JavaScript
Auf dem Bildschirm sehen beide Varianten in dieser Lektion gleich aus. Der Unterschied zeigt sich erst, wenn jemand die Maus weglegt oder sich die Seite vorlesen lässt.
Das trifft mehr Leute, als man denkt. Wer ein gebrochenes Handgelenk hat, bedient eine Woche lang alles mit der Tastatur. Und Tastaturbedienung ist ohnehin die Hälfte der Miete: Was ohne Maus geht, geht meistens auch mit Screenreader.
Was JavaScript kaputtmachen kann
Drei Dinge passieren immer wieder, und alle drei entstehen erst durch Code.
Ein div bekommt einen Klick-Handler und sieht danach aus wie ein Knopf. Ein Panel wird eingeblendet, und der Fokus bleibt irgendwo dahinter stehen. Eine Meldung erscheint auf dem Bildschirm, und wer sie nicht sieht, erfährt nie davon.
Erstens: das richtige Element nehmen
<p>Drück auf der Seite die Tabulatortaste und danach Enter.</p>
<div id="falsch" class="knopf">Ich bin ein div</div>
<button id="richtig" class="knopf" type="button">Ich bin ein button</button>
<p id="protokoll">Noch nichts passiert.</p> body {
font-family: system-ui, sans-serif;
padding: 1rem;
}
.knopf {
display: block;
margin-bottom: 8px;
padding: 8px 12px;
border: 1px solid #57534e;
border-radius: 6px;
background: #f5f5f4;
font: inherit;
cursor: pointer;
} const protokoll = document.getElementById("protokoll");
function melden(wer) {
protokoll.textContent = `${wer} wurde ausgelöst.`;
console.log(wer, "wurde ausgelöst.");
}
// Beide reagieren auf einen Mausklick, der Code ist derselbe.
document.getElementById("falsch").addEventListener("click", () => {
melden("Das div");
});
document.getElementById("richtig").addEventListener("click", () => {
melden("Der button");
});
// Probier es auf der Seite aus: Mit Tab springst du nur auf
// den zweiten. Auf dem ersten landest du gar nicht, und ohne
// Fokus gibt es auch keine Eingabetaste, die etwas auslöst. Probier das Beispiel aus. Klick erst auf die Seite, damit sie den Fokus hat, dann drück Tab. Du landest auf dem button und niemals auf dem div. Gemessen ist das ein Tab: Der div steht gar nicht in der Reihenfolge, er wird nicht übersprungen, er kommt nicht vor. Noch ein Tab, und du bist aus der Seite heraus, denn es gibt nur dieses eine bedienbare Element.
Der Grund ist einfach: Ein div ist ein Behälter ohne Bedeutung. Es steht nicht in der Reihenfolge der bedienbaren Elemente, es meldet sich einem Screenreader nicht als Knopf, und es reagiert auf keine Taste.
Ein button bringt all das mit, ohne dass du eine Zeile dafür schreibst: Er ist mit Tab erreichbar, reagiert auf Enter und die Leertaste, meldet sich als „Schaltfläche” und wird beim Deaktivieren korrekt übersprungen.
Man kann ein div nachrüsten, mit tabindex="0", role="button" und einem eigenen keydown-Handler für zwei Tasten. Das sind vier Zeilen für etwas, das ein einziges Wort im Markup umsonst liefert, und wer eine davon vergisst, merkt es nicht. Nimm das Element, das gemeint ist. Das gilt genauso für a bei einem Ziel, input bei einer Eingabe und label bei einer Beschriftung.
Zweitens: den Fokus führen
<button id="oeffnen" type="button" aria-expanded="false" aria-controls="panel">
Einstellungen
</button>
<div id="panel" hidden>
<p><button id="erster" type="button">Erster Eintrag</button></p>
<p><button type="button">Zweiter Eintrag</button></p>
</div> body {
font-family: system-ui, sans-serif;
padding: 1rem;
}
#panel {
margin-top: 8px;
padding: 8px;
border: 1px solid #d6d3d1;
} const oeffner = document.getElementById("oeffnen");
const panel = document.getElementById("panel");
const erster = document.getElementById("erster");
function oeffnen() {
panel.hidden = false;
oeffner.setAttribute("aria-expanded", "true");
// Erst einblenden, dann fokussieren. Ein verstecktes
// Element nimmt keinen Fokus an.
erster.focus();
}
function schliessen() {
panel.hidden = true;
oeffner.setAttribute("aria-expanded", "false");
// Der Fokus muss zurück, sonst steht er im Nichts und
// die nächste Tabulatortaste beginnt wieder ganz oben.
oeffner.focus();
}
oeffner.addEventListener("click", () => {
if (oeffner.getAttribute("aria-expanded") === "true") {
schliessen();
} else {
oeffnen();
}
});
panel.addEventListener("keydown", (ereignis) => {
if (ereignis.key === "Escape") {
schliessen();
}
}); Alles, was du ein- und ausblendest, braucht eine Fokusverwaltung. Sonst passiert Folgendes: Du öffnest ein Menü, der Fokus steht weiter auf dem Knopf dahinter, und die nächste Tabulatortaste springt an irgendeine Stelle, die mit dem Menü nichts zu tun hat.
Drei Regeln reichen für die meisten Fälle.
Beim Öffnen den Fokus hineinsetzen, meist auf den ersten bedienbaren Eintrag. Und zwar nachdem das Element sichtbar ist: Ein verstecktes Element nimmt keinen Fokus an, ein focus() davor läuft ins Leere.
Das kannst du nachsehen, und du musst es sogar, denn Fokus ist die eine Sache in dieser Lektion, die man nicht ansieht. Die Eingabezeile der Console beantwortet document.activeElement.id mit dem Element, auf dem er liegt. Probier es im zweiten Beispiel, solange das Panel noch zu ist (document.getElementById("panel").hidden sagt true): Tipp document.getElementById("erster").focus() und frag danach document.activeElement.tagName. Die Antwort ist BODY. Der Aufruf hat nichts getan, nichts gemeldet und nichts geworfen.
Die Aufgabe zu dieser Lektion prüft genau das, und du kannst es ausprobieren. Dreh in oeffnen() die beiden Zeilen um, also ersterEintrag.focus() vor menue.hidden = false, und drück Prüfen. Neun von zehn Prüfungen bleiben grün, und rot wird die vierte: „Der Fokus liegt auf dem ersten Eintrag”. Eine vertauschte Zeilenreihenfolge, und die Tastaturbedienung ist hin, ohne dass sich auf dem Bildschirm das Geringste ändert.
Beim Schließen den Fokus zurückholen, auf das Element, von dem aus geöffnet wurde. Sonst verliert der Nutzer seine Stelle im Dokument.
Escape schließt. Das ist eine Erwartung, die aus jedem Betriebssystem kommt, und sie kostet vier Zeilen.
Dazu gehört aria-expanded am auslösenden Knopf. Es sagt einem Screenreader, ob gerade offen oder geschlossen ist, und der Wert ist ein Text, nicht ein Wahrheitswert: "true" oder "false" in Anführungszeichen. In Lektion 13.3 gab es dasselbe Muster schon einmal, dort hieß es aria-invalid und stand an einem Feld mit Fehler.
Drittens: Änderungen ankündigen
<p><button id="speichern" type="button">Speichern</button></p>
<h2>Richtig: die Region steht schon da</h2>
<p id="status" aria-live="polite"></p>
<h2>Falsch: die Region entsteht erst mit der Meldung</h2>
<div id="behaelter"></div> body {
font-family: system-ui, sans-serif;
padding: 1rem;
}
h2 {
font-size: 1rem;
margin-bottom: 0;
}
#status,
#behaelter {
min-height: 1.5rem;
} const status = document.getElementById("status");
const behaelter = document.getElementById("behaelter");
let laeufe = 0;
document.getElementById("speichern").addEventListener("click", () => {
laeufe = laeufe + 1;
// Richtig: die Region ist beim Laden da, der Screenreader
// beobachtet sie, und nur der Text ändert sich.
status.textContent = `Gespeichert (${laeufe}).`;
// Falsch: hier entsteht die Region gerade erst. Was schon
// beim Einfügen dasteht, gilt nicht als Änderung und wird
// deshalb meist nicht vorgelesen.
behaelter.textContent = "";
const meldung = document.createElement("p");
meldung.setAttribute("aria-live", "polite");
meldung.textContent = `Gespeichert (${laeufe}).`;
behaelter.append(meldung);
}); Ein Screenreader liest die Seite von oben nach unten vor. Ändert sich danach irgendwo etwas, bekommt er davon nichts mit, es sei denn, die Stelle ist als Live-Region markiert.
aria-live="polite" heißt: Sag Bescheid, wenn sich hier der Text ändert, aber unterbrich nichts. Das ist der richtige Wert für fast alles, für Statusmeldungen, Suchergebnisse und Zähler. aria-live="assertive" unterbricht sofort und gehört nur an echte Fehlermeldungen.
Der entscheidende Punkt steht im zweiten Teil des Beispiels: Die Region muss beim Laden schon im Markup stehen. Beobachtet wird sie erst ab dem Moment, in dem sie existiert. Wer sie zusammen mit ihrem Inhalt einfügt, hat nie eine Änderung, sondern nur ein neues Element, und das wird in der Regel nicht vorgelesen.
Praktisch heißt das: Ein leeres p mit aria-live="polite" steht von Anfang an in der Seite, und dein Code setzt nur noch textContent.
Der Punkt, an dem alle stolpern
Man kann Barrierefreiheit nicht ansehen. Auf dem Bildschirm sieht das kaputte div genauso aus wie der richtige button.
Deshalb gibt es genau einen Test, der wirklich hilft und zwei Minuten dauert: Leg die Maus weg und geh mit Tab durch deine Seite. Kommst du überall hin? Siehst du, wo du gerade bist? Lässt sich alles auslösen, was sich anklicken lässt? Kommst du aus einem geöffneten Panel wieder heraus?
Wenn du dir bei einer Stelle nicht sicher bist, frag document.activeElement.id in der Eingabezeile der Console. Das ist die ehrlichste Antwort, die es gibt: Sie sagt dir, wo der Fokus wirklich liegt, und nicht, wo ein Rahmen gerade zu sehen ist.
Wenn du dabei irgendwo hängen bleibst, hast du deinen Fehler gefunden. Und wenn du nirgends hängen bleibst, ist der größte Teil der Arbeit schon getan.
Zum Mitnehmen
Ein div mit Klick-Handler funktioniert mit der Maus und mit sonst nichts. Ein button kann Fokus, Eingabetaste und Leertaste von selbst.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe im Editor, direkt im Browser geprüft
Öffnet sich mit dem Basis Konto.