axe-core 4.11.0 · axe.js:24321-24345
Warum meldet axe Kontrastfehler auf Text, den du lesen kannst?
Die Regel tut genau das, was sie ankündigt. axe-core läuft vom Textknoten aus durch jeden übergeordneten Stacking Context nach oben, multipliziert den Alphawert der Vordergrundfarbe mit der Deckkraft jedes einzelnen, legt das Ergebnis auf das, was dahinter gemalt ist, und legt das am Ende auf Weiß. Eine Überschrift ein Prozent in eine 800 ms lange Einblendung hinein ist deshalb nicht fast schwarz auf Papier — sie ist #eaf2f0 auf #edf5f3, und dieses Paar misst 1.02:1 gegen eine Anforderung von 4.5:1.
Dieses Komposit ist die Seiten-Tinte bei ungefähr ein bis zwei Prozent Deckkraft, und man kann es ohne Leuchtdichteformel nachrechnen: #eaf2f0 unterscheidet sich vom Papier, auf dem es liegt, um drei in Rot, Grün und Blau. Fast die ganze Tinte ist vom Compositing weggewaschen worden. axe irrt sich nicht in diesen Pixeln. Es irrt sich im Moment, und das ist ein anderer Fehler und braucht eine andere Lösung.
Zwei weitere Details der Regel werden wichtig, sobald eine Zahl nahe an der Grenze landet. axe schneidet das Verhältnis ab, statt zu runden — abgerundet aus Verhältnis mal hundert, geteilt durch hundert — und der Bestehenstest verlangt echt größer als die Anforderung, ein Paar mit exakt 4.50:1 fällt also durch. Beides kommt in Abschnitt 06 zurück, wo ein echter Defekt vierzehn Hundertstel unter der Grenze lag.
// lib/commons/color/get-foreground-color.js — der Fehlalarm
function calculateBlendedForegroundColor(fgColor, context, stacks) {
while (context) {
if (context.opacity === 1 && context.ancestor) {
context = context.ancestor; continue;
}
fgColor.alpha *= context.opacity; // die Tinte wird blasser
fgColor = flattenColors(fgColor, bgColor); // dann auf den Hintergrund gelegt
context = context.ancestor;
}
return fgColor;
}
// lib/checks/color/color-contrast-evaluate.js — der übersehene Fehler
if (!isVisibleOnScreen(node)) {
this.data({ messageKey: 'hidden' });
return true; // ein scharfes Reveal gilt als BESTANDEN
}
// lib/commons/dom/visibility-methods.js
function opacityHidden(vNode) {
return vNode.getComputedStylePropertyValue('opacity') === '0';
}
Der zweite Block ist die Hälfte, die in dieser Geschichte meist fehlt, und er ist die gefährlichere. Ein Fehlalarm kommt mit Selektor, Schweregrad und Knotenzahl, also streitet jemand darüber. Ein übersehener Fehler kommt als gar nichts.
two stored runs · 6 min 24 s apart
Was hat die geänderte Scan-Bedingung wirklich verändert?
Zwei Durchläufe über diese Seite liegen in tests/.golden-runs/, sechs Minuten und vierundzwanzig Sekunden auseinander, am 22. August 2026. Beide scannten 144 Seiten: 72 Dokumente bei 1440x900 und noch einmal bei 390x844. Beide spielten axe-core 4.11.0 aus node_modules ein statt von einem CDN, begrenzt auf die Tags wcag2a, wcag2aa, wcag21a und wcag21aa. Der einzige Unterschied zwischen ihnen ist eine Playwright-Context-Option: reducedMotion auf reduce statt auf no-preference.
Der erste Lauf meldete 707 schwerwiegende Farbkontrast-Knoten auf 60 Routen — 419 in Desktop-Breite, 288 im Handy-Viewport — in 117 Befundgruppen, jede davon dieselbe Regel im selben Schweregrad. Der zweite meldete 618 auf 36 Routen. Neunzig Knoten weniger klingt nach einem Rundungsfehler. Familie für Familie ist es nichts dergleichen.
| Seitenfamilie | Routen | Ohne reduce | Unter reduce |
|---|---|---|---|
| Startseite | 3 | 6 | 0 |
| Projektübersicht | 3 | 60 | 0 |
| Disziplinenübersicht | 3 | 33 | 0 |
| Disziplin-Detailseite | 12 | 292 | 0 |
| Kontakt | 3 | 118 | 0 |
| Fallstudien | 33 | 192 | 612 |
| 404 | 3 | 6 | 6 |
Fünf Familien gingen von Hunderten schwerwiegender Knoten auf exakt null. Das sind 509 Knoten reines Rauschen — Text, den ein Mensch einwandfrei liest, mitten im Reveal von einem Scanner mit Stoppuhr erwischt. Eine Familie ging in die andere Richtung und stieg um 420. Eine bewegte sich überhaupt nicht.
Die Kontrolle, die dieses Argument braucht, steckt im selben Lauf. Von den 62 eigenen Routen, die das Manifest damals führte, fand der Durchlauf ohne reduce auf 60 Verstöße; die zwei, zu denen er nichts zu sagen hatte, waren Impressum und Datenschutz — die einzigen zwei Dokumente der Seite ohne Reveal-System beim Eintritt. Die zehn migrierten Demoseiten, zusammen zwanzig Scans, steuerten aus demselben Grund null der 707 bei: Auf ihnen bewegt sich nichts.
Die Fallstudien-Familie zählte damals 33 Routen statt der 30, die sie heute zählt, weil sie zehn Fallstudien in drei Sprachen plus eine eigenständige Erklärseite umfasste, die inzwischen stillgelegt und umgeleitet ist.
Dieselbe Frage — wie viel eines gemeldeten Unterschieds der Seite gehört und wie viel dem Harness — hat auch eine Antwort in Pixeln: das Rauschniveau eines Canvas unter Visual Regression.
axe.js:26148 · 11317 · runs A and B
Die andere Hälfte: axe lässt Text bei null Deckkraft durch
Die Fallstudien-Familie meldete 192 Knoten ohne reduzierte Bewegung und 612 mit ihr. Diese Richtung ist die überraschende, und der Mechanismus sind zwei Zeilen des Werkzeugs. Der Kontrast-Check beginnt mit der Frage, ob der Knoten auf dem Bildschirm sichtbar ist; das Deckkraft-Prädikat antwortet nein für jedes Element, dessen berechnete Deckkraft exakt null ist; und der Check gibt true zurück, also bestanden.
Jedes Reveal auf dieser Seite wird über einen Startzustand mit null Deckkraft scharfgestellt, der nur dann in die Kaskade geschrieben wird, während ein Observer das Element bereits hält. Unter no-preference waren die meisten dieser Elemente auf einer Fallstudie während des Scan-Durchlaufs nie in den Viewport gekommen, standen also bei exakt null — verborgen, übersprungen, als bestanden gezählt. Unter reduce wird der Startzustand gar nicht erst geschrieben, also wurden alle 612 Knoten ausgewertet, und alle 612 fielen durch.
Der Kommentar im Kopf des Harness in diesem Repository dreht das falsch herum, in einem Satz einer sonst ungewöhnlich sorgfältigen Passage: Er sagt, ein Reveal unterhalb des Falzes stehe aus Sicht eines Scanners für immer scharf da und falle auf jeder Seite durch, auf der es vorkommt. Es passiert das Gegenteil. Ein scharfes Reveal wird stillschweigend durchgelassen, und die zwei gespeicherten Läufe belegen es — die Familie, deren Reveals meistens nie ausgelöst wurden, lieferte die kleinste Zahl, nicht die größte.
Deshalb ist die verbreitete Formulierung — axe liefere Fehlalarme bei Animationen — eine halbe Wahrheit, und die fehlende Hälfte ist die teure. Eine Regel ohne Zeitbegriff irrt sich in beide Richtungen zugleich: Sie erfindet Fehler auf Text, der gerade hereinkommt, und entschuldigt Fehler auf Text, der noch gar nicht da ist.
derived · linear fade · 800 ms reveal
Wie viel einer Einblendung ist garantiert ein Fehler?
Der Übergangspunkt lässt sich für jedes Paar ausrechnen, und für dein eigenes lohnt es sich, weil er dir sagt, welcher Anteil jeder Transition auf deiner Seite nicht scanbar ist. Lege die Tinte bei jedem Alphawert auf das Papier, wende die WCAG-Formel für relative Leuchtdichte an und schneide so ab, wie axe abschneidet.
Für die Tokens dieser Seite wird die Grenze 4.5:1 bei einer Deckkraft von 0.5521 überschritten und die Grenze 3:1 für große Schrift bei 0.4271. Unterhalb dieser Punkte ist jedes Frame ein gemeldeter schwerwiegender Verstoß gegen SC 1.4.3; oberhalb besteht dasselbe Element, ohne dass sich auf der Seite irgendetwas geändert hätte außer der Wartezeit des Scanners.
Bei einem 800 ms langen Reveal läge eine lineare Einblendung rund 442 ms unter der Grenze für normalen Text und rund 342 ms unter der für große Schrift. Nimm diese Millisekunden als Größenordnung und nicht mehr — jedes Reveal hier ist geeast, die echte Zeit unter der Grenze unterscheidet sich also von Abschnitt zu Abschnitt. Die Deckkraft-Übergänge sind exakt, die Dauern sind eine Näherung.
Das Harness macht das Rauschen dann schon durch seinen Aufbau unvermeidlich. Es scrollt jedes Dokument in Schritten von Viewport-Höhe, 60 ms auseinander, kehrt nach oben zurück, wartet 120 ms und spielt axe ein. Ein Element, das im letzten Schritt in den Blick kam, ist rund 180 ms in eine 800 ms lange Einblendung hinein, wenn axe anfängt, seinen Baum zu bauen — deutlich vor dem Übergang bei 55 Prozent, und allein deshalb als schwerwiegender Fehler gemeldet.
Der reinste Fall von allen ist das Label des Seiten-Loaders. Eingeschwungen ist es #040504 auf #21f1a8: 13.81:1, mehr als das Dreifache dessen, was die Regel verlangt. Es blendet nach 900 ms Halten über 300 ms aus, und der Lauf ohne reduce hat es genau dort erwischt — sechs Knoten, drei Startseiten, nur Desktop. Unter reduce steht es in jedem gespeicherten Lauf auf null. An der Seite hat sich nichts geändert. Nur das Frame.
three guarantees · one gate that checks
Versteckt ein Scan unter reduzierter Bewegung nicht einfach das Problem?
Das würde er, wenn die Seite unter reduzierter Bewegung die animierte Seite wäre, eingefroren in ihrem ersten Frame. Das ist die Variante dieser Technik, der man misstrauen sollte: die Einstellung anschalten, die Zahl fallen sehen, die kleinere veröffentlichen und über die Bedingung schweigen. Die Frage, die beide trennt, lautet, ob der eingeschwungene Zustand garantiert ist und ob etwas Unabhängiges das nachprüft.
Auf dieser Seite ist er auf drei Wegen garantiert, die nicht voneinander abhängen, und ein zweites Gate behauptet das Ergebnis nicht, sondern prüft es.
- Der Startzustand wird nie scharfgestellt Die Runtime schreibt das Attribut, das ein Reveal verbirgt, nur dann, wenn reduzierte Bewegung aus ist. Unter reduce gelangt der verborgene Startzustand überhaupt nicht in die Kaskade, es bleibt also nichts mitten in der Transition übrig, das einfrieren könnte.
- Jedes Ziel wird von vornherein eingeblendet Der Reveal-Installer läuft mit settleImmediately und wendet das Endframe jeder Animation sofort an. Der Motion-Vertrag nennt das end-state, und es ist die vorgeschriebene Antwort für jeden Effekt, dessen Ergebnis der Inhalt selbst ist.
- Das Stylesheet erzwingt die eingeschwungene Lage ohnehin Jedes der fünf Abschnitts-Stylesheets, die das Muster tragen, setzt in einer Media Query für reduzierte Bewegung zusätzlich volle Deckkraft und keinen Transform — für den Fall, dass die Einstellung mitten in der Sitzung umgelegt wird.
- Die Reset-Ebene zieht die Dauern zusammen base/reset.css setzt animation-duration und transition-duration unter dieser Einstellung auf 0.01ms, und zwar gezielt im Reset-Layer, damit nichts weiter unten es überspezifizieren kann.
- Ein zweites Gate belegt, dass nichts verborgen ist Das ältere Accessibility-Gate läuft über jeden animierten Hook, jedes Split-Text-Element und jede erzeugte Zeile auf der Seite unter reduzierter Bewegung und prüft, dass keins auf verborgener Sichtbarkeit, null Deckkraft oder beschnitten steht. Die Behauptung lautet: Die Zahl verborgener Inhalte ist null.
Es gibt auch ein Argument aus der Norm, und es ist das stärkere. Die Ausnahme Incidental zu SC 1.4.3 nimmt Text aus, der für niemanden sichtbar ist, und eine Überschrift bei einem Prozent Deckkraft ist für niemanden sichtbar. Den eingeschwungenen Zustand zu messen ist kein Ausweichen vor der Anforderung; es ist der Zustand, um den es der Anforderung geht, und der Befund war nie einer, den die Norm verlangt hat.
Das Accessibility-Dokument dieser Seite zieht in die andere Richtung, und die Spannung gehört benannt statt geglättet. Verpflichtung C-4 sagt, eine Farbe, die animiert, müsse ihre Grenze an jedem abgetasteten Punkt der Transition halten und nicht nur an den Endpunkten — wörtlich genommen macht das die 1.02:1 zu einem echten Fehler. Unterschieden wird in Wahrheit zwischen einem Zustand, in dem eine Leserin sich befindet, etwa einer scrollgetriebenen Farbüberblendung, die alle 10 px gemessen wird, und einem Zustand, den niemand jemals als Text wahrnimmt.
Reduzierte Bewegung ist lange vor einem Testschalter eine inhaltliche Entscheidung, und die härteste Variante davon ist eine Animation, die sich mit der Einstellung nicht einfach abschalten lässt.
project-detail.css:37-46 · not-found.css:53
Was die Korrektur überstanden hat, und was das Beheben gekostet hat
Bei drei Befunden in diesen Läufen ging es um Farbe und nicht um den Zeitpunkt, und erkennen ließ sich das nur, indem man die beiden Bedingungen verglich. Einer davon war groß, echt und 420 Knoten größer, als der erste Lauf gesagt hatte.
Der Fallstudien-Akzent ist der Zuschnitt des Seitengrüns, der auf Papier lesbar bleibt, wo er 4.82:1 misst. Die Fallstudienseite druckt ihn auf ihre eigene Kartenfläche, #e2eae7, und gegen diesen Grund misst dasselbe Paar 4.36:1 — vierzehn Hundertstel unter der Grenze, über 612 Knoten und jedes Fallstudien-Dokument hinweg. Es ist genau die Art Fehler, die ein Designer nie sieht und ein Scanner immer.
| Befund | Paar und Ort | Grenze | Gemessen, dann behoben auf |
|---|---|---|---|
| Fallstudien-Akzent als Fließtext | #0c7a55 auf der Karte #e2eae7, 612 Knoten, jedes Fallstudien-Dokument | 4.5:1 | 4.36:1, dann 5.55:1 |
| 404-Wasserzeichen | die Seiten-Tinte bei 10 Prozent über dem Papier, 6 Knoten auf 3 Routen | 3:1 bei 70px | 1.25:1, dann 3.12:1 |
| Zertifikats-Überschrift, Akzenthälfte | #21f1a8 auf #edf5f3, 5 Knoten auf 3 Routen | 3:1 | 1.33:1, nicht behoben |
Die Korrektur an der Fallstudie ist eine Zeile: fünfzehn Prozent der seiteneigenen Tinte in den Akzent gemischt, was ihn auf 5.55:1 auf der Karte und 6.13:1 auf Papier bringt und nichts ändert, das irgendjemand wahrnehmen kann. Das 404-Wasserzeichen ging von 10 Prozent Tinte auf 44 Prozent, das sind 3.12:1 gegen die Grenze 3:1, die bei 70 Pixeln gilt, und es bleibt der Überschrift daneben klar untergeordnet.
Beide waren innerhalb von acht Minuten nach dem Lauf behoben, der sie gefunden hat. Der nächste gespeicherte Durchlauf, sieben Minuten und achtundfünfzig Sekunden später, meldete null, und die Zahl blieb dann über neun aufeinanderfolgende Läufe an zwei Tagen bei null, bis eine Entscheidung sie auf fünf setzte.
Die 404 verdient noch einen Satz, weil sie die sauberste Kontrolle von allen ist. Auf dieser Seite bewegt sich nichts, sie meldete also unter beiden Bedingungen sechs Knoten und wurde nicht dadurch gefunden, dass irgendetwas geändert wurde. Nur die wahre Größe des Fallstudien-Defekts hing an der Bedingung — 192 gemeldet, 612 echt.
Die Dokumente, über die sich diese 612 Knoten verteilten, sind die Fallstudien selbst — zehn Builds, jeder mit seinen eigenen Messwerten.
published-measurements.json:33, 56-62
Warum sagt die veröffentlichte Zahl fünf und nicht null?
Die Überschrift des Zertifikatsabschnitts setzt ihre zweite Hälfte im seiteneigenen Akzent, #21f1a8, auf dem hellen Grund, auf dem dieser Abschnitt aufgebaut ist. Das misst 1.33:1, wo 3:1 gilt. Das Token, das es genau für diesen Fall gibt — der Akzent so weit abgedunkelt, dass er auf Papier lesbar ist — misst 3.23:1, und löst man die Kontrastgleichung für diesen Grund auf, deckelt sie den Akzent bei #0aa16c, was von dem Token, das ohnehin schon ausgeliefert wurde, nicht zu unterscheiden ist.
Dem Inhaber wurde die Messung zuerst gezeigt, und er wollte den Akzent trotzdem. Also geht er als Entscheidung mit ihrer Zahl daneben online und nicht als Feinjustierung: fünf Knoten, ein Element, drei Routen, zwei Viewports. Von einer Ausnahme ist er auch nicht gedeckt — die Logotype-Klausel deckt ein Logo oder einen Markennamen, und eine Abschnittsüberschrift in einer Markenfarbe ist Text wie jeder andere.
- 144 Scans 72 Dokumente in zwei Breiten, WCAG 2.1 A und AA
- 5 Verstöße ein Element, drei Routen, zwei Viewports
- 4.11.0 axe-core-Version aus node_modules eingespielt, nie von einem CDN
- 0 Screenreader-Durchgänge hier hat nie ein manueller Durchgang stattgefunden
Die Ausnahme, die das Gate passieren lässt, ist eine Zeile, die eine Regel auf einem Selektor benennt. Jeder andere Knoten derselben Regel irgendwo auf der Seite lässt den Build weiterhin scheitern. Und sie verrottet mit Absicht: Wird diese Überschrift jemals behoben, passt die Zeile nicht mehr, und der Lauf schlägt fehl, bis jemand sie löscht — denn eine Ausnahme, die niemand braucht, ist eine Lüge im Repository.
Der Matcher hat eine scharfe Kante, die man klar aussprechen sollte. Er prüft nur den ersten fehlschlagenden Knoten jeder Befundgruppe, wer also eine Gruppe entschuldigt, entschuldigt jeden Knoten darin. Was diese Grobheit begrenzt, ist die Zahl: Das Gate gibt die Knotensumme aller Entschuldigungen aus, die Seite veröffentlicht sie, und die Summe ist fünf. Dahinter ist kein Platz, in dem sich noch etwas verstecken könnte.
Diese Rechnung zu bezahlen brauchte drei Stellen und eine vierte, die sie zusammenhält — den veröffentlichten Eintrag, die sichtbare Fassung auf Englisch, Deutsch und Arabisch und die eine Ausnahme im Harness, dazu ein Fakten-Gate, das prüft, ob Scan-Zahl, Dokumentzahl, Toolversion und Verstoßzahl im Text noch zum Eintrag passen. Eine Zahl, die irgendwo steht, wo kein Gate hinreicht, wird schal, und eine in diesem Repository ist es schon.
Die Seite, die Leserinnen diese Zahl nennt, direkt neben dem Tastaturweg, den sie bewusst ausschließt, ist die vier Disziplinen.
144 scans · 0 keystrokes
Was ein grüner Scan trotzdem nicht sagt
Ein Scanner liest Markup. Er kann keine Tabulatortaste drücken, er kann dir nicht sagen, dass eine Überschrift falsch ist, und er kann nicht wissen, dass ein Bedienelement in der Praxis unerreichbar ist. Was diese Seite veröffentlicht, ist eine Zahl automatisiert gefundener Verstöße, und sie ist nie das Wort barrierefrei.
Die Regel, um die es hier geht, hat auf der anderen Seite derselben Seite einen passenden blinden Fleck. Deques eigene Liste der Grenzen sagt, colour-contrast melde nichts zu Text, der von einem anderen Element verdeckt wird — und jede Zeile des Reveals dieser Seite ist von einem deckenden Balken bedeckt, der wegwischt. Die sechshundert Millisekunden, die dieser Balken auf dem Schirm steht, ist der Text buchstäblich unsichtbar, und axe verbucht ein Bestanden; die achthundert Millisekunden, die eine Einblendung läuft, ist der Text für einen Menschen bei sechzig Prozent lesbar, und axe verbucht einen schwerwiegenden Fehler. Beide Fehler kommen aus derselben fehlenden Idee.
- Fahre den Durchlauf zweimal
- Einmal unter prefers-reduced-motion: reduce und einmal ohne, dann vergleiche beide. Was beide Läufe übersteht, ist eine Frage der Farbe. Was nur in einem auftaucht, eine Frage des Zeitpunkts.
- Prüfe, worauf deine Reveals stehen
- Exakt null Deckkraft wird als verborgen durchgelassen. Ein Scan, der deine Reveals nie auslöst, sagt dir nicht, dass sie in Ordnung sind — er sagt dir überhaupt nichts über sie.
- Rechne deinen eigenen Übergangspunkt aus
- Lege deine Tinte auf dein Papier und finde den Alphawert, bei dem sie 4.5:1 überschreitet. Dieser Anteil ist der Teil jeder Transition auf deiner Seite, den ein Scanner nicht ehrlich lesen kann.
- Behebe einen Befund nie, indem du die Animation änderst
- Wenn die eingeschwungenen Farben bestehen, geht es bei dem Befund um die Stoppuhr des Scanners. Eine Dauer zu kürzen, damit ein Bericht grün wird, verschiebt den Defekt, statt ihn zu entfernen.
- Veröffentliche die Knotenzahl von allem, was du entschuldigst
- Eine benannte Ausnahme mit einer Zahl daneben kann nicht heimlich wachsen, und eine, die nicht mehr passt, sollte den Build scheitern lassen, statt unbemerkt in der Datei zu liegen.
Zwei weitere Grenzen gehören in denselben Atemzug. Eine Route, die nicht lädt, liefert keine Befunde, und liegt sie außerhalb des gewerteten Satzes, lässt sie den Lauf auch nicht scheitern — ein gespeicherter Durchlauf scannte 143 Seiten statt 144, weil eine Demo-Route nach dreißig Sekunden in einen Timeout lief. Und eine veröffentlichte Zahl ist ein Beleg über einen Build an einem Datum: Der Routensatz hier hat sich seit dem 24. August 2026 bereits geändert, und genau deshalb sagt der Eintrag selbst, dass vor dem Deployment neu gemessen werden muss.
Beim Kontrast ist die alte Seite das nützliche Gegenbeispiel. Ihr axe-Lauf über 40 Routen in zwei Viewports lieferte 379 Verstoßknoten, alle Farbkontrast, aus nur vier verschiedenen Farbpaaren zwischen 1.26:1 und 1.47:1 — derselbe Akzent auf vier fast weißen Gründen. Überhaupt kein Rauschen mitten im Reveal, weil jenes Harness die Seite in Schritten von 600 Pixeln las und vor dem Scannen 800 ms wartete, und weil ihre Eintritte fertig waren. Dasselbe Werkzeug, dieselbe Regel, völlig anderes Rauschen. Die Scan-Prozedur ist eine Variable, und sie gehört neben die Zahl, die sie erzeugt hat.
Wenn ein Bericht über deinen eigenen Build Zahlen enthält, die niemand erklären kann, ist das meist ein Problem der Bedingung und nicht der Farbe — sprich es durch.