Die Hacker-AG hat eine Website, die gut aussieht und trotzdem viele aussperrt: Wer nur die Tastatur benutzt, kommt nicht zum Knopf, wer schlecht sieht, kann den Text kaum lesen, und der Cookie-Banner trickst. In diesem Labor findest du die Barrieren und die Tricks und baust sie aus, Schritt für Schritt geprüft. Im dritten Teil geht es um Mails, die so tun, als kämen sie von deiner Schule.
Die Seite der Hacker-AG hat einen Fehler, den man nicht sieht, wenn man sie mit der Maus und guten Augen benutzt. Alle anderen merken ihn sofort: Wer blind ist, lässt sich die Seite von einem Screenreader vorlesen, einem Programm, das ihr HTML liest. Wer die Maus nicht halten kann, springt mit der Tastatur von Link zu Link. Wer schlecht sieht, vergrößert die Seite oder braucht kräftige Farben. Und manche Barrieren treffen alle einmal: helle Sonne auf dem Handy, ein gebrochener Arm, ein Beamer, der Farben verschluckt.
Die Regeln für eine Seite, die alle benutzen können, stehen in den Web Content Accessibility Guidelines (WCAG), im Handbuch unter Barrierefreiheit. Hier wendest du sie an.
Öffne ein Terminal in deinem Workspace und hol dir die Seite der AG:
mkdir -p fair-web/bilder cd fair-web wget https://workspace.hackschule.de/fair-web/index.html https://workspace.hackschule.de/fair-web/style.css https://workspace.hackschule.de/fair-web/script.js wget -P bilder https://workspace.hackschule.de/fair-web/bilder/logo.svg https://workspace.hackschule.de/fair-web/bilder/ag.svg
Öffne fair-web/index.html in VS Code und zeig sie mit »Go Live« im Browser
an, wie in Statische Webseiten. Sieh sie dir eine Minute lang
an: Ein Banner für Cookies, ein Menü, ein Bild, Termine, ein Formular.
Leg die Maus zur Seite. Klick einmal in die Seite im Browser, damit sie die Tastatur bekommt; mit Tab springst du dann zum nächsten Link, Knopf oder Feld, mit Shift+Tab zurück, mit Enter drückst du einen Knopf.
Bevor du es ausprobierst: Wie oft musst du auf der Seite der AG Tab drücken, bis der Knopf »Anmelden« unter dem Formular gewählt ist?
Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.
Der Knopf »Anmelden« ist gar nicht erreichbar: Er ist ein Element div mit
onclick, und ein div bekommt die Tastatur nie. Das Formular lässt sich so
ohne Maus nicht abschicken. Und du siehst nicht einmal, wo du bist, weil
outline: none in style.css den Rahmen um das gewählte Element weggenommen
hat.
Ein Screenreader liest nicht, was auf dem Bildschirm steht, sondern das HTML dahinter. So klingt eine Seite, deren HTML nicht sagt, was sie ist:
Auf deinem Handy oder Rechner ist ein echter Screenreader eingebaut: VoiceOver auf dem iPhone und dem Mac, TalkBack auf Android, die Sprachausgabe von Windows (Strg+Windows+Enter schaltet sie an und aus). Wer ihn einmal eine Minute lang benutzt hat, baut Seiten anders.
Ein Bild ist für einen Screenreader nur eine Datei, bis das Attribut alt sagt,
was es zeigt. Fehlt es, liest er den Dateinamen vor, Buchstabe für Buchstabe
oder als »ag punkt s v g«. Ein guter Alt-Text sagt, was ein Mensch auf dem Bild
sieht und was davon für die Seite wichtig ist, in einem Satz. Ein Bild, das nur
schmückt, bekommt alt="": Dann überspringt der Screenreader es.
Die Seite hat zwei Bilder. Das Logo neben dem Menü sagt, wessen Seite das ist. Das große Bild zeigt den Raum der AG:
Welcher Alt-Text passt zum großen Bild der AG?
Die Sprache der Seite gehört in das erste Element, <html lang="de">. Fehlt
sie, liest ein Screenreader, der auf Englisch eingestellt ist, den deutschen
Text mit englischer Aussprache vor, und die Silbentrennung des Browsers rät.
<html> in fair-web/index.html sagt, in welcher Sprache die Seite ist.
Jedes Bild in fair-web/index.html hat ein alt, das sagt, was es zeigt, und keinen Dateinamen.
In ein Feld mit placeholder steht ein grauer Text, solange es leer ist. Er
verschwindet, sobald man tippt, und manche Screenreader lesen ihn gar nicht.
Was in ein Feld gehört, sagt ein <label>, verbunden mit dem Feld über
for und id:
<label for="name">Dein Name</label> <input id="name" type="text" autocomplete="name">
Ein Klick auf den Text setzt dann auch den Cursor ins Feld, eine größere
Fläche zum Treffen. autocomplete sagt dem Browser, was hinein soll, damit er
es vorschlagen kann: name, email, tel.
Ein Fehler, den nur ein roter Rahmen zeigt, sieht niemand, der Rot und Grün schlecht unterscheidet, und ein Screenreader sagt nichts davon. Zu einem Fehler gehört ein Satz, der sagt, was fehlt, neben dem Feld:
<p id="name-error" class="error-text" hidden>Bitte gib deinen Namen ein.</p>
Das Attribut hidden versteckt ihn, bis script.js es entfernt; mit
aria-describedby="name-error" am Feld liest der Screenreader ihn zum Feld
vor.
Jedes Feld in fair-web/index.html, auch das Häkchen, hat ein <label>.
Ein <label for="name"> gehört zu dem Feld mit id="name". Die drei Felder haben ihre id schon.
Beim Häkchen wird der Satz daneben zum Label: <label for="newsletter">…</label> um den Text.
Ein <button> kann von selbst, was ein Knopf braucht: Er bekommt die Tastatur,
reagiert auf Enter und die Leertaste, und der Screenreader sagt
»Schalter«. Ein <div onclick> kann nichts davon. Im Formular schickt ein
<button type="submit"> das Formular ab; script.js fängt das mit einem
Listener für submit ab, statt mit onclick an einem Element:
document.getElementById('form').addEventListener('submit', (event) => { event.preventDefault(); // hier prüfen, was eingetippt wurde });
Auch das »↑« rechts unten ist ein <div>. Was zu einer Stelle der Seite
springt, ist ein Link: <a href="#" class="top">Nach oben</a>.
Screenreader haben eine Liste aller Links einer Seite, damit man nicht alles anhören muss. Auf der Seite der AG steht dort zweimal »hier klicken«.
Ein Screenreader zeigt auf Wunsch eine Liste aller Links der Seite, mit ihrem Text. Was steht für die Seite der AG darin?
Ein Link sagt, wohin er führt: <a href="#termine">Termine</a>. Und ein
Menü ist ein <nav>, das kommt im Abschnitt »Struktur«.
In fair-web/index.html ist nichts mehr ein <div> mit onclick, weder »Anmelden« noch das »↑«.
Wenn »Anmelden« ein <button type="submit"> ist, ruft niemand mehr send() auf. Häng den Code in script.js an das Formular, wie im Codeblock oben.
Wie gut sich Text von seinem Hintergrund abhebt, misst das Kontrastverhältnis, von 1 : 1 (Grau auf demselben Grau) bis 21 : 1 (Schwarz auf Weiß). Die WCAG verlangen für normalen Text mindestens 4,5 : 1, für großen Text (ab 24 Pixel, fett ab knapp 19) mindestens 3 : 1.
Und manches sagt die Seite nur mit Farbe. Wer eine Rot-Grün-Schwäche hat, in Europa etwa jeder zwölfte Mann, sieht Rot und Grün als ähnliche Brauntöne:
Wie ein Browser das Verhältnis ausrechnet, steht in den WCAG, und du kannst es
selbst programmieren. Zuerst bekommt jede Farbe eine relative Leuchtdichte
\(L\) zwischen 0 (Schwarz) und 1 (Weiß). Eine Farbe wie #2b8a3e besteht
aus drei Kanälen, Rot 2b, Grün 8a und Blau 3e, jeder eine Zahl von 0 bis
255 in hexadezimaler Schreibweise. Jeder Kanal wird erst durch 255 geteilt,
dann linearisiert: Ein Bildschirm macht die Zahlen nicht gleichmäßig hell,
die Hälfte von 255 leuchtet viel weniger als halb so hell.
$$c' = \frac{c}{255}, \qquad c_{\mathrm{lin}} = \begin{cases} \dfrac{c'}{12{,}92} & \text{wenn } c' \le 0{,}04045 \\[1.2ex] \left(\dfrac{c' + 0{,}055}{1{,}055}\right)^{2{,}4} & \text{sonst} \end{cases}$$
Dann zählt jeder Kanal so viel, wie das Auge ihn wahrnimmt, Grün am meisten, Blau am wenigsten:
$$L = 0{,}2126 \cdot R_{\mathrm{lin}} + 0{,}7152 \cdot G_{\mathrm{lin}} + 0{,}0722 \cdot B_{\mathrm{lin}}$$
Schreib luminance(color). color ist ein String wie '#2b8a3e'; die
Funktion gibt die relative Leuchtdichte nach den Formeln oben zurück,
0 für Schwarz und 1 für Weiß.
color[1, 2] sind die beiden Zeichen nach dem #, also Rot, color[3, 2] Grün, color[5, 2] Blau. '8a'.to_i(16) liest sie als hexadezimale Zahl: 138.
Für jeden Kanal: durch 255.0 teilen, dann mit if c <= 0.04045 die eine oder die andere Formel. ** ist die Potenz: x**2.4.
Ein Gerüst:
ruby
def luminance(color)
channels = [color[1, 2], color[3, 2], color[5, 2]].map do |hex|
c = hex.to_i(16) / 255.0
# hier: c linearisieren
end
# hier: die drei Kanäle gewichtet addieren
end
Schreib luminance(color). color ist ein String wie '#2b8a3e'; die
Funktion gibt die relative Leuchtdichte nach den Formeln oben zurück,
0 für Schwarz und 1 für Weiß.
color[1:3] sind die beiden Zeichen nach dem #, also Rot, color[3:5] Grün, color[5:7] Blau. int('8a', 16) liest sie als hexadezimale Zahl: 138.
Für jeden Kanal: durch 255 teilen, dann mit if c <= 0.04045 die eine oder die andere Formel. ** ist die Potenz: x ** 2.4.
Ein Gerüst:
python
def luminance(color):
channels = []
for i in (1, 3, 5):
c = int(color[i:i + 2], 16) / 255
# hier: c linearisieren und an channels anhängen
# hier: die drei Kanäle gewichtet addieren
Das Verhältnis zweier Farben ist dann der Quotient ihrer Leuchtdichten, die hellere oben, mit 0,05 auf beiden Seiten für das Licht, das immer auf einen Bildschirm fällt:
$$\mathrm{Kontrast} = \frac{L_{\mathrm{hell}} + 0{,}05}{L_{\mathrm{dunkel}} + 0{,}05}$$
Bevor du rechnest: Reicht #777777, ein mittleres Grau, auf Weiß für
normalen Text, also für 4,5 : 1?
Schreib contrast_ratio(a, b): das Kontrastverhältnis zweier Farben,
egal in welcher Reihenfolge sie kommen. Kopiere deine Funktion
luminance aus der Aufgabe darüber dazu.
luminance('#000000') ist 0, luminance('#ffffff') ist 1. Mit der Formel: (1 + 0,05) / (0 + 0,05) = 21.
[x, y].max und [x, y].min geben die größere und die kleinere von zwei Zahlen.
Schreib contrast_ratio(a, b): das Kontrastverhältnis zweier Farben,
egal in welcher Reihenfolge sie kommen. Kopiere deine Funktion
luminance aus der Aufgabe darüber dazu.
luminance('#000000') ist 0, luminance('#ffffff') ist 1. Mit der Formel: (1 + 0,05) / (0 + 0,05) = 21.
max(x, y) und min(x, y) geben die größere und die kleinere von zwei Zahlen.
Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.
#777777 auf Weiß hat 4,48 : 1 und reicht knapp nicht; #767676, eine
Stufe dunkler, hat 4,54 : 1 und ist das hellste Grau, das auf Weiß für
normalen Text genügt. Mit bloßem Auge siehst du den Unterschied nicht. Darum
rechnet man nach.
style.css nach: #999999 auf Weiß, die grünen und roten Termine, der weiße Text auf dem blauen Knopf. Welches Paar ist am schlechtesten?
Ändere style.css, bis jede Schrift auf ihrem Hintergrund 4,5 : 1 erreicht,
auch im Banner und auf dem Knopf. Dunkler wird es, wenn du die Zahlen
kleiner machst: #333333 statt #999999. Damit der ausgefallene Termin nicht
nur rot ist, schreib dazu, was mit ihm ist: <strong>(fällt aus)</strong>.
Jede Schrift in fair-web/style.css erreicht auf ihrem Hintergrund mindestens 4,5 : 1.
Die Meldung nennt die Regel, die Farben und ihr Verhältnis. Rechne mit deiner Funktion nach, wie dunkel die Schrift sein muss.
Eine Farbe wird dunkler, wenn ihre drei Zahlen kleiner werden, #2b8a3e zu #1e6b30. Auf einem farbigen Hintergrund mit weißer Schrift wird der Hintergrund dunkler.
Ein Screenreader springt von Überschrift zu Überschrift, so wie du beim
Überfliegen einer Seite. Die Ebenen sagen dabei, was wozu gehört: Nach einem
<h1> kommt <h2>, darunter <h3>. Die Seite der AG springt von <h1>
gleich zu <h4>, weil <h4> in style.css so groß ist, wie es aussehen
sollte. Die Größe einer Überschrift gehört ins CSS, ihre Ebene ins HTML.
Dazu kommen die Bereiche der Seite: <nav> für das Menü, <main> für den
Inhalt. Mit ihnen springt ein Screenreader direkt zum Inhalt, statt jedes Mal
das Menü anzuhören.
Die Überschriften in fair-web/index.html überspringen keine Ebene.
Aus <h4> wird <h2>. Damit die Überschriften so groß bleiben, ändere in style.css auch h4 { in h2 {.
Wer mit der Tastatur arbeitet, muss sehen, wo er gerade ist. Browser zeichnen
dafür einen Rahmen um das Element, das den Fokus hat; outline: none in
style.css nimmt ihn allen weg. Lösch die Regel und gib dem Fokus einen
eigenen, gut sichtbaren Rahmen. :focus-visible gilt nur, wenn die Tastatur
den Fokus gebracht hat, nicht bei einem Klick mit der Maus:
:focus-visible { outline: 3px solid #1a5fb4; outline-offset: 2px; }
fair-web/style.css gibt :focus-visible einen Rahmen mit outline.
Was blinkt, lenkt ab, und manche Menschen bekommen von Bewegung auf dem Bildschirm Kopfschmerzen oder Schwindel. Im Betriebssystem lässt sich »Bewegung reduzieren« einschalten, und eine Seite kann das abfragen:
@media (prefers-reduced-motion: reduce) { .scarcity { animation: none; } }
Ob die Zeile mit dem Countdown überhaupt bleiben sollte, kommt im nächsten Teil.
fair-web/style.css hält die Animation an, wenn jemand »Bewegung reduzieren« eingeschaltet hat.
Browser haben Werkzeuge, die vieles davon selbst finden. In Chrome und Edge heißt es Lighthouse (F12, Reiter »Lighthouse«, nur »Barrierefreiheit« ankreuzen), in Firefox gibt es im selben Fenster den Reiter »Barrierefreiheit«. Lass deine Seite prüfen.
Was ein Werkzeug nicht findet: ob ein Alt-Text zum Bild passt, ob ein Link sagt, wohin er führt, ob die Reihenfolge der Seite Sinn ergibt. Dafür braucht es einen Menschen, am besten einen, der die Seite wirklich mit Screenreader oder Tastatur benutzt.
Websites von Behörden müssen barrierefrei sein und eine Erklärung zur Barrierefreiheit haben, die sagt, was es noch nicht ist (das Handbuch, Was das Gesetz sagt).
Lass Lighthouse oder den Reiter »Barrierefreiheit« von Firefox über deine Seite laufen. Schreib dann in ein paar Sätzen eine Erklärung zur Barrierefreiheit für die Seite der AG: Was hast du geändert, was ist noch nicht barrierefrei, und was kann kein Werkzeug für dich prüfen?