golden:webgl §7 · 4 checks
Was sollte reduzierte Bewegung mit einem Canvas-Hero machen?
Zwei Antworten werden häufig ausgeliefert, und beide sind falsch. Die erste lässt den Hero weiterlaufen, denn eine Media Query im Stylesheet erreicht nichts, was eine JavaScript-Laufzeit in einen GPU-Kontext zeichnet — die Einstellung ist gesetzt, die Seite sieht identisch aus, und wer um weniger Bewegung gebeten hat, bekommt exakt genauso viel davon. Die zweite entfernt den Hero und tauscht ein bewegtes Bild gegen ein Loch, samt dem gesamten ersten Eindruck der Seite.
Die Antwort hier ist ein ganzer Frame, und danach nichts. Unter reduce sind das Porträt, das Halbton-Raster und die angedockte Pose gezeichnet; die Pointer-Parallaxe und die Fluid-Spur nicht; und keine Frame-Registrierung bleibt bestehen. Entschieden wird das in einem einzigen Prädikat in der Laufzeit des Heros, wo die Einstellung einer von vier unabhängigen Gründen zum Anhalten ist, und der else-Zweig ist das, was aus dem Ergebnis einen ruhenden Zustand statt eines leeren macht: die Frame-Phasen abmelden, den Pointer schließen, dann noch einmal rendern, falls der Hero noch in seinem Gate liegt.
Der Rest dieses Artikels ist diese Entscheidung, verallgemeinert über neun Subsysteme, die drei Fehler, die es dafür zu beheben galt, und die Zahlen hinter den vier Werten unten.
- 61 Gate-Prüfungen, die die Einstellung nennen von 817 in 17 Prüflisten
- 7 Canvas-Elemente mit weiterhin sichtbarem Bild 1 WebGL-Hero, 1 Punktfeld, 5 Rive-Pfeile
- 0 gehaltene Frame-Registrierungen an jeder der 12 Scroll-Positionen
- 16 reduce-Blöcke in den Stylesheets verteilt auf 17 von 42 Dateien
src/styles/base/reset.css:38-49
Der Notausschalter ist richtig, und er erreicht weniger, als du denkst
Der Ausschnitt unten wird hier unverändert ausgeliefert, im ersten Cascade Layer, und nichts in diesem Artikel ist ein Argument dagegen. Er ist die Untergrenze. Wissenswert ist, warum er im ersten Layer steht und was er nachweislich nicht erreichen kann.
Bei important-Deklarationen dreht sich die Rangfolge der Cascade Layer um: Eine normale Deklaration gewinnt im späteren Layer, eine important-Deklaration im früheren. Die Layer-Reihenfolge lautet reset, tokens, base, layout, components, sections, utilities, overrides — das hier ist also die stärkste important-Regel im Stylesheet, und ein Unit-Test prüft beide Hälften davon: Layer null ist reset und enthält die Duration-Zeile, während der letzte Layer überhaupt keine important-Deklaration trägt.
/* src/styles/base/reset.css — der ERSTE Cascade Layer. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
/* dokumentierte Ausnahme: Nutzereinstellung schlägt Design */
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* For important declarations the layer order REVERSES: the declaration
in the earliest layer wins (CSS Cascade 5, section 6.1). Nothing in a
later layer can out-rank this at any specificity — and the correct
escape hatch is therefore a NORMAL declaration in the last layer.
Reaching for important there makes the rule weaker, not stronger. */
Die zwei Zeilen, in denen es nicht um Dauer geht, sind tragend. 0.01ms statt 0s erhält die Events transitionend und animationend, sodass eine Zustandsmaschine, die auf das Ende einer Transition wartet, weiterläuft; animation-iteration-count: 1 verhindert, dass eine als infinite deklarierte Animation bei 0.01ms zu einem sehr schnellen Flackern wird — dem Gegenteil dessen, worum gebeten wurde. Chris Coyier, der den Ausschnitt bekannt gemacht hat, hält im selben Beitrag fest, dass er nur dort greift, wo Bewegung vollständig aus CSS kommt, und dass er bei JavaScript-getriebenen Effekten Animationen schnell und schwindelerregend machen kann.
Den Schalter nach vorn zu stellen, hat einen ehrlichen Preis, und dieses Repository hält ihn fest, statt ihn zu umgehen. Für das Lade-Overlay war shortened bei 0,3 s spezifiziert — und das ist aus keinem späteren Layer erreichbar, denn eine normale Deklaration verliert gegen die important-Deklaration, und eine important-Deklaration in einem späteren Layer ist noch schwächer. Unter reduce steht die Marke also fertig da, statt gezeichnet zu werden, und der Burst ist augenblicklich. Das ist eine bewusste Verschlechterung gegenüber der Referenz, die so bleibt, weil die Nutzereinstellung über dem Design zu stehen das richtige Ergebnis ist.
Hier gehört eine Schlussfolgerung hin, keine Messung. Drei Regeln in späteren Layern dieses Repositorys setzen unter reduce transition-duration: 0.01s, und nach der Kaskadenregel aus dem Absatz oben sind das normale Deklarationen, die von der important-Deklaration im reset-Layer dominiert werden. Kein Gate im Harness liest unter reduce eine berechnete Transition-Dauer aus, das folgt also aus der Spezifikation und nicht aus einem Browser — genau die Sorte Regel, die sich in einer Codebasis ansammelt, ohne dass jemand merkt, dass sie wirkungslos ist.
home-hero.css:329-353
Die Dauer ist der falsche Hebel — die Frage ist, auf welchem Frame du landest
Der Hero trägt ein Wireframe, das sich in einem 18-Sekunden-Zyklus selbst zeichnet und wieder löscht. Stauche diesen Zyklus auf 0.01ms, und jeder Strich landet auf dem Keyframe, der gerade da ist — bei dieser Animation ist das der gelöschte: Die Pauschalregel hinterlässt, korrekt angewandt und genau das tuend, was sie sagt, eine leere Fläche. An der Dauer lässt sich auch nichts verhandeln, denn diese important-Deklaration ist die stärkste im Stylesheet.
Eine einzige Eigenschaft setzt sie außer Kraft. animation-name: none nimmt die Animation vollständig aus dem Spiel, und keine Dauer kann eine Animation wiederbeleben, die keinen Namen hat.
/* src/styles/sections/home-hero.css — the hero wireframe.
The reset layer would collapse the 18 s cycle to 0.01 ms and land
every stroke on the ERASED keyframe, leaving an empty board. */
@media (prefers-reduced-motion: reduce) {
.wfx-e {
animation-name: none;
stroke-dashoffset: 0;
}
.wfx-g2,
.wfx-g3,
.wfx-g4 {
display: none;
}
}
/* Eine Gruppe bleibt voll gezeichnet — genau dafür ist das Artboard da. */
Dieselbe Falle erwischt den Hinweis zur Geräteausrichtung, wo drei Elemente ein Handy animieren, das sich aufrichtet, während dahinter ein Bogen gezeichnet wird. Ihr erster Frame ist das liegende Gerät ohne Bogen — die eine Pose, die keine Information trägt — also parkt der reduce-Block alle drei stattdessen im Endzustand: das Handy aufrecht, die Marke aufrecht dazu, der Bogen vollständig gezeichnet und sein Kopf beleuchtet. Das ist end-state im Vokabular der vier Antworten, und die Begründung ist der allgemeine Fall: Die Information ist ein Ziel und keine Reise, also verliert eine Leserin, die um weniger Bewegung gebeten hat, die Geste und behält die Bedeutung.
Der dritte Fall ist kleiner und leicht zu übersehen. Der Cursor im Code-Panel blinkt über eine keyframes-Regel, die der Tipp-Controller nie für sich beansprucht — die Tipp-Kette startet also gar nicht, und das Blinken wäre das Einzige geblieben, was sich auf der Seite noch bewegt. Es wird im eigenen reduce-Block des Abschnitts namentlich stillgelegt, aus demselben Grund wie das Wireframe.
motion-architecture.md:451-460
Vier Antworten, und warum runs eine schriftliche Begründung braucht
Die Einstellung ist kein Boolean, und die Spezifikation beschreibt sie auch nicht so. Media Queries Level 5 definiert sie als Erkennen des Wunsches nach weniger Bewegung auf der Seite, und MDN beschreibt die Pflicht der Autorin als ein Interface, das bewegungsbasierte Animationen entfernt, reduziert oder ersetzt — drei erlaubte Reaktionen statt einer. Das ist die externe Grundlage für eine Taxonomie, und diese Website deklariert für jeden animierten Effekt eine von vier Antworten.
| Antwort | Was sie bedeutet | Wo sie hier vorkommt |
|---|---|---|
| off | Startet nie. Das Element rendert in seinem fertigen Endzustand | Der Hamburger-Morph, das Touch-Ziehen der Leiste, die Pointer-Platte |
| end-state | Die Animation entfällt, ihr letzter Frame wird sofort angewandt | Der WebGL-Hero, die fünf Rive-Pfeile, das Punktfeld, jedes Einblenden |
| shortened | Läuft, mit verkürzter Dauer | Für den Lade-Burst benannt, als aus späterem Layer unerreichbar vermerkt |
| runs | Läuft unverändert und braucht eine schriftliche Begründung | Genau eine Szene auf der ganzen Website: das Kapitel der Hintergrundfarbe |
Die Deklaration ist keine Review-Konvention. Ein Effekt lässt sich ohne reset- und dispose-Funktion gar nicht bei der Motion-Runtime registrieren, und die Registrierung wirft einen TypeError, wenn eine davon fehlt — ein Effekt, dessen ehrliche Antwort nichts ist, schreibt eine leere Funktion mit einem Kommentar, der sagt warum. Schweigen ist ein Absturz und keine Vorgabe, und genau das macht aus der Architekturregel etwas, das ein Build halten kann.
Das eine runs auf der Website ist eine Farb-Überblendung, die an der Scroll-Position hängt, und sie verdient die Ausnahme mit einer Messung statt einer Meinung: Nichts verschiebt, dreht oder skaliert sich, es gibt also keine Bewegung zu reduzieren. Sie abzuschalten, zerbrach messbar die Seite bei 390 px, wo das reduzierte Stylesheet den Wrapper des Rings auf einen Viewport zusammenzieht, die Fortschrittsquelle des Kapitels auf null geht und der Hintergrund über das ganze Kapitel hinweg rgb(237, 245, 243) hält. Das committete Gate prüft die Gegenbedingung, mit gepinntem Ring und Weg auf null: Der berechnete Hintergrund des body muss bis zum Deck immer noch unter einen Rotkanal von 120 abdunkeln.
7 canvases · 3 runtimes · 15 consumers
Subsystem für Subsystem: was reduce tatsächlich tut
Ein einziges Auslesen der Einstellung erreicht fünfzehn Laufzeitmodule, und jedes davon bringt eine andere Fläche zur Ruhe. Die Tabelle ist die Referenz: die Antwort, woraus der ruhende Zustand besteht, und die committete Prüfung, die fehlschlagen würde, wenn er es nicht wäre.
| Subsystem | Antwort | Woraus der ruhende Zustand besteht | Committete Prüfungen |
|---|---|---|---|
| WebGL-Hero, Three.js | end-state | Ein ganzer Frame bei Delta null; mehr als die halbe Fläche beleuchtet; kein Pointer registriert | 5 |
| Rive-Pfeile, Canvas2D | end-state | Zehn Schritte von je einer Sechzigstelsekunde, einmal angewandt und gezeichnet | 3 |
| Punktfeld, Canvas2D | end-state | Die ruhende Streuung mit jedem abgetasteten Punkt, der Cursor-Ring außerhalb des Feldes geparkt | 5 |
| GSAP-Reveals | off | Gar nicht erst installiert: Text unbeschnitten, keine Wischer, alles als eingeblendet markiert | 3 |
| ScrollTrigger-Scrubs | off | Die gestaltete Pose des Abschnitts — der Ring eingefroren bei den minus 20 Grad aus dem Stylesheet | 6 |
| Lenis-Glättung | off | Nie konstruiert: natives Scrollen, sofort, und jede Methode funktioniert weiterhin | 1 |
| Zertifikats-Deck | off | Sechs vollständige Karten in einer einfachen Spalte, das Zertifikat zuletzt, kein Pause-Button | 7 |
Zwei Zeilen lohnen eine Ausführung, denn sie zeigen, was ein ruhender Zustand im Bau kostet. Rives Antwort ist als Wert im Contract deklariert, statt in einem Zweig entschieden zu werden — der ausgelieferte Pfeil-Contract trägt settle — und zur Ruhe bringen heißt, die Zustandsmaschine zehnmal um je eine Sechzigstelsekunde vorzurücken und einmal zu zeichnen. Die Grenze ist fest und keine Schleife, die wartet, bis die Maschine sich nicht mehr ändert, denn eine schleifende Zustandsmaschine erreicht diese Bedingung nie. Zehn Schritte reichen, damit ein Übergang zwischen zwei Ruhezuständen ankommt.
Das Punktfeld hat einen eigenen Standbild-Modus, und der ist ausdrücklich nicht Frame null. Im normalen Lauf tastet ein Drittel der Punkte seinen Driftvektor in jedem Frame neu ab, der Rest verwendet den letzten weiter — das steckt im ausgelieferten Look und ist nicht nachträglich angeschraubt, Frame null ist also ein Drittel Bild. Der Standbild-Modus tastet jeden Punkt ab und parkt den Cursor-Ring weit außerhalb des Feldes, sodass der Frame unter reduzierter Bewegung die ruhende Streuung ist und kein eingefrorener Cursor-Klecks. Das Gate prüft die Füllung statt der Schleife: dieselbe Zahl gezeichneter Pixel und derselbe Alpha-Mittelwert im Abstand von 900 ms, und das Feld weiterhin im aktiven Satz, denn ein Umschalten der Einstellung, das das Feld aushängt, nimmt die Deckungsmessung des Headers mit.
pinTime(1.5) against pinTime(9)
Der Halbframe-Fehler, und die Frage, die ihn gefunden hat
Der ruhende Frame des Heros war monatelang falsch und sah die ganze Zeit richtig aus. Gefunden wurde er nicht durch Hinsehen, sondern durch die Forderung, dass der Frame deterministisch sein muss — das ist der übertragbare Teil dieser Geschichte: Auf einem Canvas sind die Korrektheitsfragen, die du mit dem Auge beantworten kannst, genau die schon beantworteten.
- Die Implementierung, die korrekt aussieht Unter reduce die Frame-Phasen abmelden und dann die Write-Phase einmal aufrufen. Es zeichnet, das Canvas ist nicht leer, und ein Screenshot davon überzeugt.
- Der schlummernde Fehler Write setzt mit dem zusammen, was die letzte Compute-Phase hinterlassen hat. Wo nie ein Compute gelaufen ist, sind das ein Zeit-Uniform von 0 und eine Hintergrundrausch-Textur auf null — dem ruhenden Frame fehlte also das eigene Feld des Halbton-Rasters, und das ist der größte Teil dessen, wie der Hero aussieht.
- Die Frage, die ihn ans Licht brachte Der Harness verlangte Determinismus statt Schönheit. Zwei Aufnahmen bei fixierten Zeiten von 1,5 s und 9 s kamen byteweise identisch zurück, und eine fixierte Zeit, die nichts ändert, ist ein Uniform, das keinen Shader erreicht.
- Die Behebung Compute laufen lassen und dann Write, bei einem Delta von null. Jeder Dämpfer behält seinen Wert, denn Dämpfung über keine Zeit gibt den Ausgangswert zurück, und die verstrichene Zeit rückt nicht vor. Es ist derselbe Frame, zu Ende gezeichnet, und nicht der nächste.
- Das Gate, das die Behauptung beweisbar macht 3 Sekunden fixieren, einmal rendern, dann prüfen, dass das Zeit-Uniform der Szene 3 liest. Erst zu fixieren ist Pflicht: Unter reduce ist eine nicht fixierte Zeit berechtigterweise 0, dort abgelesen beweist sie also in keine Richtung etwas. Ein halber Frame lässt sie bei 0 stehen, mit der Rausch-Textur auf null.
- Zurücklesen, niemals screenshotten Der Drawing Buffer wird nicht erhalten, der Canvas-Inhalt ist also undefiniert, sobald der Compositor ihn genommen hat, und ein Element-Screenshot ist ein Münzwurf. Im selben Durchgang rendern und Pixel lesen ist die einzige ehrliche Aufnahme.
Die Prüfliste um diese Behebung sichert vier Dinge zu, und keines davon ist ein Bild: mehr als die halbe Canvas-Fläche ist beleuchtet, der Frame ist ein ganzer Frame, kein Frame wird gehalten und kein Pointer ist registriert, und die Frame-Zahl ist 1.200 ms später unverändert. Das Letzte wiegt schwerer, als es klingt — ein Renderer, der still vor sich hin weitertickt, erfüllt jede statische Prüfung, die du über das Bild schreiben kannst.
Pixel zurückzulesen, statt Screenshots zu vergleichen, ist dieselbe Disziplin, die ein Canvas für Regressionstests braucht — dazu gibt es hier eigene Zahlen: das Rauschniveau eines Canvas-Screenshots.
home-journey.css:569-605
Die Hälfte, die keine Dauer-Regel erreicht: Layout und Zustand
Drei Abschnitte dieses Repositorys kamen unabhängig voneinander und mit eigenen Worten zum selben Befund: Der Schalter kann Layout nicht zurücknehmen, und er kann Zustand nicht zurücknehmen. Eine sticky-Platte pinnt weiterhin. Ein Transform, das ein Controller geschrieben hat, hält weiterhin. Eine Custom Property, die auf irgendeinen Zwischenwert gescrubbt wurde, behält, was zuletzt hineingeschrieben wurde. Nichts davon ist noch eine Animation, wenn die Einstellung gelesen wird, und nichts davon hat eine Dauer, die sich stauchen ließe.
Bei der Journey-Leiste hat das eine messbare Menge Inhalt gekostet. Sie ist ein horizontales System — vier Karten auf einer 4.052 px breiten Bahn in einem 1.440 px breiten Viewport — und das Einzige, was die anderen drei ins Bild holt, ist ein gescrubbtes Transform. Dieses Transform für reduce abzuschalten, ist richtig, denn ein scrollgetriebenes seitliches Gleiten ist genau die Bewegung, um die es bei der Einstellung geht; nur das abzuschalten, hat drei Viertel des Abschnitts hinter dem horizontalen Overflow des body abgeschnitten. Vor der Layout-Regel gemessen, kam das Dokument auf 14.884 px statt 18.709, und die fehlenden 3.825 waren die Kapitel, die niemand erreichen konnte.
Ein kürzeres Dokument unter reduce ist richtig, und das Gate sagt das ausdrücklich: Die reduzierte Seite an ihrer animierten Länge zu messen, hieße zu behaupten, dass reduzierte Bewegung nichts ändert — das Gegenteil des Punkts. Gelten muss, dass kein Inhalt verloren geht, die Prüfung sammelt also jedes Journey-Panel ein und verlangt von jedem, dass es Fläche hat, dargestellt und sichtbar ist und innerhalb des horizontalen Bands des Dokuments liegt. Die Behebung gibt reduce dieselbe Darstellung wie einem Handy: Aus der Bahn wird eine Spalte.
Die Zustandshälfte davon erzeugt einen schlimmeren Fehler als eine fehlende Karte. Auf der Projektleiste löst der reduce-Block die Bühne aus dem sticky-Zustand, stellt horizontales Overflow und Scroll-Snapping wieder her, setzt das Transform der Bahn auf none und touch-action zurück auf auto — alles, damit ein veraltetes Attribut, das eine mitten in der Sitzung geänderte Einstellung hinterlässt, bevor die Runtime abgebaut hat, keine Leserin in einem acht Bildschirme langen Pin festhalten kann, aus dem sie nicht herausscrollt. touch-action wiederherzustellen ist keine Dekoration: Das Drag-Skript wird unter reduce nie installiert, ein geerbtes pan-y würde also genau die Geste verbieten, für die es den Fallback überhaupt gibt.
Die Website, die diese hier ersetzt hat, hatte die feinste Variante dieser Klasse. Ihr Seitenübergangs-Burst verkürzte sich unter reduce von 0,68 s auf 0,3 s, während die damit gepaarte Sichtbarkeits-Transition bei 0,68 s blieb — für rund 380 ms nach dem Verschwinden des Overlays landeten die Klicks eines Besuchers mit reduzierter Bewegung also auf einer unsichtbaren Ebene über dem ganzen Viewport. Eine nur teilweise angewandte Einstellung entkoppelt zwei Zeiten, die nur zusammen jemals richtig waren, und das sieht man weder auf einem Screenshot noch in einer automatisierten Prüfung.
Die Leiste, die diese zwei Regeln schützen, ist das Projektverzeichnis, und die Builds dahinter sind die zehn Konzept-Websites.
hero-swipe-lock.js:34-41
Was reduce nicht abschalten darf
Der Test lautet nicht: bewegt es sich. Eine Scroll-Sperre bewegt nichts, und sie unter reduce abzuschalten, würde eine Navigationshilfe zerstören, um die der Besucher ausdrücklich gebeten hat; ein Pin bewegt nichts mehr, wenn die Einstellung gelesen wird, und ihn stehen zu lassen, setzt die Leserin fest. Diese Linie pro Mechanismus zu ziehen — auch mitten durch eine Komponente, wenn dort die Naht verläuft — ist der größte Teil der Arbeit.
- eine Scroll-Sperre
- Kein Bewegungssystem, also schaltet reduce sie nicht ab — Scrollen zu sperren ist eine Navigationshilfe, um die der Besucher ausdrücklich gebeten hat. Die zwei Label-Wechsel daneben sind Bewegung, und sie schrumpfen auf einen sofortigen Wechsel. Die Sperre selbst wird vom Scroll-Besitzer referenzgezählt, dieses Bedienelement und das Menü können also je eine halten, ohne die des anderen freizugeben.
- ein Pin
- Layout, also muss reduce ihn zurücknehmen, statt ihn nur nicht mehr zu animieren. Die committete Barrierefreiheits-Prüfung übernimmt eine Zusicherung, die die alte Website nie ausgeführt hat: Unter reduce, bei geöffnetem Menü, lassen 1.200 px Mausrad, ein Druck auf End und ein Druck auf PageDown die Scroll-Position unverändert, und Escape gibt sie frei und lässt die Seite ihr eigenes Ende erreichen.
- Smooth Scrolling
- Ein anderer Modus statt eines abgeschalteten. Auf der Website, die diese hier ersetzt hat, ließ der naheliegende Griff — den Smoother anzuhalten — die Seite messbar völlig unscrollbar zurück: 0 px vom Mausrad, 0 px von End. Deshalb pollte ihr Patch bis zu 600 Frames lang nach einer globalen Variable und zerstörte stattdessen die Instanz. Hier wird der Smoother unter reduce schlicht nie konstruiert, und das Scrollen ist nativ und vollständig.
- ein Pause-Bedienelement
- Abwesend statt wirkungslos. Das Zertifikats-Deck teilt sechs Karten im Takt von 2.200 ms mit 600 ms Austeilen aus, eine Runde dauert 13,2 s, und unter reduce baut es sich gar nicht erst auf — der Button, der es anhalten würde, wird also entfernt statt deaktiviert. Ein Bedienelement, das da ist und nichts tut, ist schlimmer als eines, das fehlt: Es kostet einen Tabstopp, einen zugänglichen Namen und einen Druck.
- eine Farb-Überblendung
- Der eine Effekt dieser Website, der runs deklariert. Nichts verschiebt, dreht oder skaliert sich, und das Argument ist gemessen und nicht behauptet — mit abgeschaltetem Effekt ging bei 390 px der eigene Weg des Kapitels auf null, und die Seite hielt über ihre ganze Länge eine flache Farbe. Reduzierte Bewegung ist auf einem großen Teil der Handys standardmäßig an, was die Arbeitsannahme dieses Repositorys ist und keine von ihm gemessene Zahl.
Den Scroll-Fall gibt es auch in der Bibliothek selbst, und dort hat er sich bewegt. Die hier gepinnte Version, 1.1.20, enthält überhaupt keinen Verweis auf die Query — sie nie zu konstruieren war also die einzige verfügbare Antwort. Aktuelles Lenis liefert respectReducedMotion standardmäßig aktiviert aus und bezieht eine dritte Position: die Instanz weiterlaufen lassen, mit auf eins zu eins gezwungener Interpolation, damit die Synchronisation von WebGL und DOM erhalten bleibt, und programmatische Scrolls sofort ausführen. Drei vertretbare Antworten auf eine Einstellung — das Taxonomie-Argument im Kleinen.
Ob sich ein Smoother überhaupt lohnt, ist eine eigene Messung, und die steht hier: Lenis gegen natives Scrollen, gemessen.
Die Antwort Sofortsprung hat für Anker eigene Folgen für Fokus und History — was ein weicher Ankerlink kaputt macht.
axe-core 4.11.0 · 144 scans · 2026-08-24
Wie du es beweist, und die 707, aus denen 5 wurden
Die Aussage über die ganze Seite besteht aus zwei Zusicherungen im selben Durchgang, und sie sind die These in ausführbarer Form. Mit gesetzter Einstellung hält kein Subsystem einen Frame — die Zahl der aktiven Registrierungen an der Frame-Uhr ist null, der Hero ist nicht registriert, keine Rive-Instanz läuft, die Partikel-Runtime ist geparkt. Und jede 2-D-Fläche trägt weiterhin ein ruhendes Bild: Kein Canvas der sieben kommt mit null gezeichneten Pixeln zurück.
Der Round-Trip zur Laufzeit ist die schwerere Hälfte, denn eine Einstellung kann sich mitten in der Sitzung ändern. reduce einzuschalten baut 26 der 41 ScrollTrigger ab und jeden Pin-Spacer mit ihnen — dorthin gehen die 3.203 px Dokumenthöhe; es auszuschalten baut sie wieder auf, ohne dass über die Runde ein Effekt leckt oder sich verdoppelt, und ohne dass in einer der beiden Richtungen etwas beschnitten bleibt. Der Scroll-Offset überlebt das nicht — 9.645 px, dann 8.004, dann 9.716 — und diese 71 px Drift schieben drei weitere Reveals unterhalb der Falz in die Menge der noch nicht ausgelösten.
| Prüfliste | Prüfungen darin | Nennen die Einstellung |
|---|---|---|
| certificate | 123 | 7 |
| webgl | 72 | 5 |
| projects | 52 | 6 |
| rive | 48 | 3 |
| particles | 38 | 5 |
| rest | 20 | 7 |
| accessibility | 20 | 9 |
| alle 17 Prüflisten | 817 | 61 |
Das ist keine rhetorische Behauptung. Die veröffentlichte axe-core-Zahl dieser Website — 5 Verstöße, axe-core 4.11.0, 72 Dokumente in zwei Viewports, also 144 Durchläufe, gemessen am 2026-08-24 — trägt eine Bedingung, und die Bedingung ist tragend: prefers-reduced-motion: reduce, nachdem jedes Dokument von Anfang bis Ende gescrollt wurde. Ohne sie ausgeführt, meldete dasselbe Instrument auf derselben Website 707 schwerwiegende Farbkontrast-Knoten über 60 Routen. Die zwei Durchgänge decken unterschiedlich viele Routen ab, sie sind also dasselbe Werkzeug vor und nach einer korrigierten Bedingung und kein kontrollierter Vergleich.
Jeder einzelne dieser 707 war ein Element mitten im Einblenden. Axe rechnet ein teiltransparentes Element gegen das, was dahinter liegt, eine Farbe wird also gegen eine fast identische Farbe gemessen, und eine einblendende Überschrift kommt auf 1,02:1. Die Korrektur hat nichts entschuldigt: Zwei echte Fehler haben sie überlebt und wurden behoben — eine Fallstudien-Karte, die über 612 Knoten und alle 33 ihrer Dokumente mit 4,36:1 druckte, und ein 404-Wasserzeichen mit 1,25:1 — und die verbleibenden 5 sind ein Element auf dem Papier des Zertifikats, mit 1,33:1 dort, wo 3:1 gilt, namentlich und per Selektor über drei Routen und zwei Breiten erlaubt.
Die Website, die diese hier ersetzt hat, ist das Gegenbeispiel zu alldem, und ihre Zahlen sind Forensik jenes alten Builds, nicht dieses hier. Ein grep nach der Query über ihre Engine liefert null. Unter reduce lief ihr GL-Tick 51-mal in drei Sekunden gegen 52 ohne, ihre globale GSAP-Timeline blieb über alle 836 ihrer Reveals bei einem Time Scale von 1, ihre 61 ScrollTrigger liefen unverändert, und genau eine Sorte Rive-Canvas pausierte — der Nav-Hamburger, 70 davon auf der ganzen Website — während 520 andere weiterspielten. Die Behandlung, die es gab, war ein 106-Zeilen-Patch, der nach einer globalen Variable pollte, plus 24 Stylesheet-Blöcke in zwei zueinander unverträglichen Richtlinien. Es ist nicht so, dass es niemand versucht hätte. Es ist so, dass die Einstellung dort beantwortet wurde, wo es leicht war, und dort unbeantwortet blieb, wo die Bewegung tatsächlich saß.
Die Kontrast-Hälfte dieser Geschichte ist ein eigener Text — warum ein axe-Durchlauf einer animierten Website meist Fehlalarme meldet.