In diesem Artikel wird beschrieben, warum und wie wir die Simulation von Farbsehschwäche in DevTools und im Blink-Renderer implementiert haben.
Hintergrund: schlechter Farbkontrast
Text mit geringem Kontrast ist das häufigste automatisch erkennbare Problem mit der Barrierefreiheit im Web.
Laut der WebAIM-Analyse der Barrierefreiheit der 1 Million beliebtesten Websites weisen über 86% der Startseiten einen geringen Kontrast auf. Im Durchschnitt gibt es auf jeder Startseite 36 unterschiedliche Instanzen von Text mit niedrigem Kontrast.
Kontrastprobleme mit den Entwicklertools finden, verstehen und beheben
Mit den Chrome-Entwicklertools können Entwickler und Designer den Kontrast verbessern und barrierefreiere Farbschemas für Web-Apps auswählen:
- Die Kurzinfo zum Prüfmodus, die oben auf der Webseite angezeigt wird, enthält das Kontrastverhältnis für Textelemente.
- Die Farbauswahl in den Entwicklertools weist auf schlechte Kontrastverhältnisse für Textelemente hin, zeigt die empfohlene Kontrastlinie, um die manuelle Auswahl besserer Farben zu erleichtern, und kann sogar barrierefreie Farben vorschlagen.
- Sowohl im Übersichtsfeld für Preisvergleichsportale als auch im Lighthouse-Bericht zur Barrierefreiheit werden Textinhalte mit niedrigem Kontrast aufgeführt, die auf Ihrer Seite gefunden wurden.
Wir haben dieser Liste vor Kurzem ein neues Tool hinzugefügt, das sich etwas von den anderen unterscheidet. Die oben genannten Tools konzentrieren sich hauptsächlich darauf, Informationen zum Kontrastverhältnis bereitzustellen und Ihnen Optionen zur Behebung zu geben. Wir haben festgestellt, dass Entwickler in den DevTools noch keine Möglichkeit haben, sich ein umfassenderes Bild von diesem Problem zu machen. Um dieses Problem zu beheben, haben wir auf dem Tab „Rendering“ der Entwicklertools die Simulation von Sehschwächen implementiert.
In Puppeteer können Sie mit der neuen page.emulateVisionDeficiency(type) API diese Simulationen programmatisch aktivieren.
Farbsehschwächen
Etwa 1 von 20 Menschen leidet an einer Farbsehschwäche (auch als „Farbenblindheit“ bezeichnet, was nicht ganz korrekt ist). Solche Beeinträchtigungen erschweren es, verschiedene Farben zu unterscheiden, was Kontrastprobleme verstärken kann.
Als Entwickler mit normalem Sehvermögen sehen Sie in den DevTools möglicherweise ein schlechtes Kontrastverhältnis für Farbpaare, die für Sie visuell in Ordnung aussehen. Das liegt daran, dass in den Formeln für das Kontrastverhältnis diese Farbenfehlsichtigkeiten berücksichtigt werden. Sie können Text mit geringem Kontrast in einigen Fällen vielleicht noch lesen, aber Menschen mit Sehbehinderungen haben diese Möglichkeit nicht.
Indem Designer und Entwickler die Auswirkungen dieser Sehschwächen auf ihre eigenen Web-Apps simulieren können, möchten wir das fehlende Puzzleteil liefern: Mit den DevTools können Sie nicht nur Kontrastprobleme finden und beheben, sondern jetzt auch verstehen.
Farbsehschwächen mit HTML, CSS, SVG und C++ simulieren
Bevor wir uns mit der Blink-Renderer-Implementierung unserer Funktion befassen, ist es hilfreich zu verstehen, wie Sie eine entsprechende Funktion mit Webtechnologie implementieren würden.
Sie können sich jede dieser Simulationen als Overlay vorstellen, das die gesamte Seite abdeckt. Die Webplattform bietet dafür eine Möglichkeit: CSS-Filter. Mit der CSS-Eigenschaft filter können Sie einige vordefinierte Filterfunktionen wie blur, contrast, grayscale, hue-rotate und viele mehr verwenden. Für noch mehr Kontrolle kann die filter-Property auch eine URL akzeptieren, die auf eine benutzerdefinierte SVG-Filterdefinition verweist:
<style>
:root {
filter: url(#deuteranopia);
}
</style>
<svg>
<filter id="deuteranopia">
<feColorMatrix values="0.367 0.861 -0.228 0.000 0.000
0.280 0.673 0.047 0.000 0.000
-0.012 0.043 0.969 0.000 0.000
0.000 0.000 0.000 1.000 0.000">
</feColorMatrix>
</filter>
</svg>
Im obigen Beispiel wird eine benutzerdefinierte Filterdefinition auf Grundlage einer Farbmatrix verwendet. Konzeptionell wird der [Red, Green, Blue, Alpha]-Farbwert jedes Pixels mit einer Matrix multipliziert, um eine neue Farbe [R′, G′, B′, A′] zu erstellen.
Jede Zeile in der Matrix enthält fünf Werte: einen Multiplikator für (von links nach rechts) R, G, B und A sowie einen fünften Wert für einen konstanten Verschiebungsbetrag. Es gibt vier Zeilen: Die erste Zeile der Matrix wird verwendet, um den neuen Rotwert zu berechnen, die zweite Zeile den Grünwert, die dritte Zeile den Blauwert und die letzte Zeile den Alphawert.
Vielleicht fragen Sie sich, woher die genauen Zahlen in unserem Beispiel stammen. Warum ist diese Farbmatrix eine gute Annäherung an Deuteranopie? Die Antwort lautet: Wissenschaft! Die Werte basieren auf einem physiologisch genauen Simulationsmodell für Farbsehschwäche von Machado, Oliveira und Fernandes.
Wir haben jetzt diesen SVG-Filter und können ihn mit CSS auf beliebige Elemente auf der Seite anwenden. Wir können dasselbe Muster für andere Sehschwächen wiederholen. Hier sehen Sie eine Demo:
Wir könnten unsere DevTools-Funktion so entwickeln: Wenn der Nutzer in der DevTools-Benutzeroberfläche eine Sehschwäche emuliert, fügen wir den SVG-Filter in das untersuchte Dokument ein und wenden dann den Filterstil auf das Stammelement an. Dieser Ansatz hat jedoch mehrere Probleme:
- Möglicherweise ist für das Stammelement der Seite bereits ein Filter vorhanden, der durch unseren Code überschrieben wird.
- Möglicherweise ist auf der Seite bereits ein Element mit
id="deuteranopia"vorhanden, das mit unserer Filterdefinition in Konflikt steht. - Die Seite basiert möglicherweise auf einer bestimmten DOM-Struktur. Wenn wir das
<svg>-Element in das DOM einfügen, verstoßen wir möglicherweise gegen diese Annahmen.
Abgesehen von Grenzfall-Szenarien besteht das Hauptproblem bei diesem Ansatz darin, dass wir programmatisch beobachtbare Änderungen an der Seite vornehmen. Wenn ein DevTools-Nutzer das DOM untersucht, sieht er möglicherweise plötzlich ein <svg>-Element, das er nie hinzugefügt hat, oder ein CSS-filter, das er nie geschrieben hat. Das wäre verwirrend. Um diese Funktion in den Entwicklertools zu implementieren, benötigen wir eine Lösung, die diese Nachteile nicht hat.
Sehen wir uns an, wie wir das weniger aufdringlich gestalten können. Diese Lösung besteht aus zwei Teilen, die wir ausblenden müssen: 1) der CSS-Stil mit der Property filter und 2) die SVG-Filterdefinition, die derzeit Teil des DOM ist.
<!-- Part 1: the CSS style with the filter property -->
<style>
:root {
filter: url(#deuteranopia);
}
</style>
<!-- Part 2: the SVG filter definition -->
<svg>
<filter id="deuteranopia">
<feColorMatrix values="0.367 0.861 -0.228 0.000 0.000
0.280 0.673 0.047 0.000 0.000
-0.012 0.043 0.969 0.000 0.000
0.000 0.000 0.000 1.000 0.000">
</feColorMatrix>
</filter>
</svg>
SVG-Abhängigkeit im Dokument vermeiden
Beginnen wir mit Teil 2: Wie können wir vermeiden, dass das SVG dem DOM hinzugefügt wird? Eine Möglichkeit besteht darin, sie in eine separate SVG-Datei zu verschieben. Wir können den <svg>…</svg> aus dem oben stehenden HTML-Code kopieren und als filter.svg speichern. Dazu müssen wir aber zuerst einige Änderungen vornehmen. Für Inline-SVG in HTML gelten die HTML-Parsing-Regeln. Das bedeutet, dass Sie beispielsweise Attributwerte in einigen Fällen ohne Anführungszeichen angeben können. SVG in separaten Dateien sollte jedoch gültiges XML sein und das XML-Parsing ist viel strenger als HTML. Hier ist noch einmal unser SVG-in-HTML-Snippet:
<svg>
<filter id="deuteranopia">
<feColorMatrix values="0.367 0.861 -0.228 0.000 0.000
0.280 0.673 0.047 0.000 0.000
-0.012 0.043 0.969 0.000 0.000
0.000 0.000 0.000 1.000 0.000">
</feColorMatrix>
</filter>
</svg>
Damit daraus ein gültiges eigenständiges SVG (und damit XML) wird, müssen wir einige Änderungen vornehmen. Kannst du erraten, welche?
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="deuteranopia">
<feColorMatrix values="0.367 0.861 -0.228 0.000 0.000
0.280 0.673 0.047 0.000 0.000
-0.012 0.043 0.969 0.000 0.000
0.000 0.000 0.000 1.000 0.000" />
</filter>
</svg>
Die erste Änderung ist die XML-Namespace-Deklaration oben. Die zweite Ergänzung ist der sogenannte „Solidus“ – der Schrägstrich, der angibt, dass das <feColorMatrix>-Tag das Element sowohl öffnet als auch schließt. Diese letzte Änderung ist eigentlich nicht erforderlich (wir könnten auch einfach beim expliziten schließenden Tag </feColorMatrix> bleiben), aber da sowohl XML als auch SVG-in-HTML diese />-Abkürzung unterstützen, können wir sie auch verwenden.
Mit diesen Änderungen können wir die Datei als gültige SVG-Datei speichern und im CSS-Attribut filter unseres HTML-Dokuments darauf verweisen:
<style>
:root {
filter: url(filters.svg#deuteranopia);
}
</style>
Wir müssen SVG nicht mehr in das Dokument einfügen. Das ist schon viel besser. Aber… wir sind jetzt von einer separaten Datei abhängig. Das ist immer noch eine Abhängigkeit. Können wir es irgendwie loswerden?
Wie sich herausstellt, benötigen wir keine Datei. Wir können die gesamte Datei in einer URL codieren, indem wir eine Daten-URL verwenden. Dazu nehmen wir den Inhalt der SVG-Datei, die wir zuvor hatten, fügen das Präfix data: hinzu und konfigurieren den richtigen MIME-Typ. So erhalten wir eine gültige Daten-URL, die dieselbe SVG-Datei repräsentiert:
data:image/svg+xml,
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="deuteranopia">
<feColorMatrix values="0.367 0.861 -0.228 0.000 0.000
0.280 0.673 0.047 0.000 0.000
-0.012 0.043 0.969 0.000 0.000
0.000 0.000 0.000 1.000 0.000" />
</filter>
</svg>
Der Vorteil ist, dass wir die Datei jetzt nicht mehr irgendwo speichern oder von der Festplatte oder über das Netzwerk laden müssen, nur um sie in unserem HTML-Dokument zu verwenden. Anstatt wie zuvor auf den Dateinamen zu verweisen, können wir jetzt auf die Daten-URL verweisen:
<style>
:root {
filter: url('data:image/svg+xml,\
<svg xmlns="http://www.w3.org/2000/svg">\
<filter id="deuteranopia">\
<feColorMatrix values="0.367 0.861 -0.228 0.000 0.000\
0.280 0.673 0.047 0.000 0.000\
-0.012 0.043 0.969 0.000 0.000\
0.000 0.000 0.000 1.000 0.000" />\
</filter>\
</svg>#deuteranopia');
}
</style>
Am Ende der URL geben wir wie bisher die ID des Filters an, den wir verwenden möchten. Das SVG-Dokument muss in der URL nicht Base64-codiert werden. Das würde nur die Lesbarkeit beeinträchtigen und die Dateigröße erhöhen. Wir haben am Ende jeder Zeile Backslashes hinzugefügt, damit die Zeilenumbruchzeichen in der Daten-URL das CSS-Stringliteral nicht beenden.
Bisher haben wir nur darüber gesprochen, wie sich Sehbeeinträchtigungen mithilfe von Webtechnologie simulieren lassen. Interessanterweise ist unsere endgültige Implementierung im Blink-Renderer sehr ähnlich. Hier finden Sie ein C++-Hilfsprogramm, das wir hinzugefügt haben, um eine Daten-URL mit einer bestimmten Filterdefinition zu erstellen. Es basiert auf derselben Technik:
AtomicString CreateFilterDataUrl(const char* piece) {
AtomicString url =
"data:image/svg+xml,"
"<svg xmlns=\"http://www.w3.org/2000/svg\">"
"<filter id=\"f\">" +
StringView(piece) +
"</filter>"
"</svg>"
"#f";
return url;
}
So verwenden wir ihn, um alle erforderlichen Filter zu erstellen:
AtomicString CreateVisionDeficiencyFilterUrl(VisionDeficiency vision_deficiency) {
switch (vision_deficiency) {
case VisionDeficiency::kAchromatopsia:
return CreateFilterDataUrl("…");
case VisionDeficiency::kBlurredVision:
return CreateFilterDataUrl("<feGaussianBlur stdDeviation=\"2\"/>");
case VisionDeficiency::kDeuteranopia:
return CreateFilterDataUrl(
"<feColorMatrix values=\""
" 0.367 0.861 -0.228 0.000 0.000 "
" 0.280 0.673 0.047 0.000 0.000 "
"-0.012 0.043 0.969 0.000 0.000 "
" 0.000 0.000 0.000 1.000 0.000 "
"\"/>");
case VisionDeficiency::kProtanopia:
return CreateFilterDataUrl("…");
case VisionDeficiency::kTritanopia:
return CreateFilterDataUrl("…");
case VisionDeficiency::kNoVisionDeficiency:
NOTREACHED();
return "";
}
}
Mit dieser Technik haben wir Zugriff auf die volle Leistung von SVG-Filtern, ohne etwas neu implementieren oder neu erfinden zu müssen. Wir implementieren eine Blink Renderer-Funktion, nutzen dazu aber die Webplattform.
Wir haben also herausgefunden, wie wir SVG-Filter erstellen und in Daten-URLs umwandeln können, die wir im CSS-Eigenschaftswert filter verwenden können. Fällt Ihnen ein Problem mit dieser Technik ein? Es hat sich herausgestellt, dass wir uns nicht darauf verlassen können, dass die Daten-URL in allen Fällen geladen wird, da die Zielseite möglicherweise eine Content-Security-Policy hat, die Daten-URLs blockiert. Bei unserer endgültigen Implementierung auf Blink-Ebene wird CSP für diese „internen“ Daten-URLs beim Laden umgangen.
Abgesehen von Sonderfällen haben wir gute Fortschritte gemacht. Da wir nicht mehr darauf angewiesen sind, dass Inline-<svg> im selben Dokument vorhanden ist, haben wir unsere Lösung auf eine einzelne, in sich geschlossene CSS-filter-Property-Definition reduziert. Sehr gut! Das entfernen wir jetzt auch.
CSS-Abhängigkeit im Dokument vermeiden
Zur Erinnerung: Das ist der aktuelle Stand:
<style>
:root {
filter: url('data:…');
}
</style>
Wir sind weiterhin auf die CSS-Eigenschaft filter angewiesen, die möglicherweise ein filter im tatsächlichen Dokument überschreibt und zu Problemen führt. Außerdem würde es beim Untersuchen der berechneten Stile in den Entwicklertools angezeigt werden, was verwirrend wäre. Wie können wir diese Probleme vermeiden? Wir müssen eine Möglichkeit finden, dem Dokument einen Filter hinzuzufügen, ohne dass er für Entwickler programmatisch sichtbar ist.
Eine Idee war, eine neue Chrome-interne CSS-Property zu erstellen, die sich wie filter verhält, aber einen anderen Namen hat, z. B. --internal-devtools-filter. Wir konnten dann eine spezielle Logik hinzufügen, um dafür zu sorgen, dass diese Eigenschaft nie in den Entwicklertools oder in den berechneten Stilen im DOM angezeigt wird. Wir können sogar dafür sorgen, dass es nur für das Element funktioniert, für das wir es benötigen: das Stammelement. Diese Lösung wäre jedoch nicht ideal: Wir würden Funktionen duplizieren, die bereits mit filter vorhanden sind. Selbst wenn wir uns bemühen, diese nicht standardmäßige Eigenschaft zu verbergen, könnten Webentwickler sie trotzdem finden und verwenden, was schlecht für die Webplattform wäre. Wir brauchen eine andere Möglichkeit, einen CSS-Stil anzuwenden, ohne dass er im DOM sichtbar ist. Könnten Sie mir weiterhelfen?
Die CSS-Spezifikation enthält einen Abschnitt, in dem das verwendete visuelle Formatierungsmodell vorgestellt wird. Eines der wichtigsten Konzepte ist dabei der Viewport. Dies ist die visuelle Ansicht, über die Nutzer die Webseite aufrufen. Ein eng verwandtes Konzept ist der Initial Containing Block, der in etwa einem stilvollen Viewport <div> entspricht, der nur auf Spezifikationsebene vorhanden ist. In der Spezifikation wird immer wieder auf dieses „Darstellungsbereich“-Konzept verwiesen. Sie wissen beispielsweise, wie der Browser Bildlaufleisten anzeigt, wenn der Inhalt nicht passt. Das ist alles in der CSS-Spezifikation definiert, basierend auf diesem „Darstellungsbereich“.
Dieses viewport ist auch im Blink-Renderer als Implementierungsdetail vorhanden. Hier ist der Code, der die Standard-Darstellungsbereichsstile gemäß der Spezifikation anwendet:
scoped_refptr<ComputedStyle> StyleResolver::StyleForViewport() {
scoped_refptr<ComputedStyle> viewport_style =
InitialStyleForElement(GetDocument());
viewport_style->SetZIndex(0);
viewport_style->SetIsStackingContextWithoutContainment(true);
viewport_style->SetDisplay(EDisplay::kBlock);
viewport_style->SetPosition(EPosition::kAbsolute);
viewport_style->SetOverflowX(EOverflow::kAuto);
viewport_style->SetOverflowY(EOverflow::kAuto);
// …
return viewport_style;
}
Sie müssen kein C++-Experte sein und auch nicht die Feinheiten der Style-Engine von Blink verstehen, um zu erkennen, dass in diesem Code die z-index, display, position und overflow des Viewports (oder genauer gesagt: des anfänglichen enthaltenden Blocks) verarbeitet werden. Das sind alles Konzepte, die Sie vielleicht schon von CSS kennen. Es gibt noch weitere Besonderheiten im Zusammenhang mit Stapelkontexten, die sich nicht direkt in eine CSS-Eigenschaft übersetzen lassen. Insgesamt können Sie sich das viewport-Objekt jedoch als etwas vorstellen, das mit CSS in Blink gestaltet werden kann, genau wie ein DOM-Element – nur dass es nicht Teil des DOM ist.
Genau das wollten wir! Wir können unsere filter-Stile auf das viewport-Objekt anwenden. Das wirkt sich visuell auf das Rendern aus, ohne die beobachtbaren Seitenstile oder das DOM zu beeinträchtigen.
Fazit
Zusammenfassend lässt sich sagen, dass wir zuerst einen Prototyp mit Webtechnologie anstelle von C++ erstellt und dann begonnen haben, Teile davon in den Blink-Renderer zu verschieben.
- Zuerst haben wir unseren Prototyp eigenständiger gemacht, indem wir Daten-URLs inline eingefügt haben.
- Wir haben diese internen Daten-URLs dann CSP-freundlich gemacht, indem wir das Laden speziell behandelt haben.
- Wir haben unsere Implementierung DOM-agnostisch und programmatisch nicht beobachtbar gemacht, indem wir Stile in die Blink-interne
viewportverschoben haben.
Das Besondere an dieser Implementierung ist, dass unser HTML/CSS/SVG-Prototyp das endgültige technische Design beeinflusst hat. Wir haben einen Weg gefunden, die Webplattform zu nutzen, sogar im Blink-Renderer.
Unser Designvorschlag und der Chromium-Tracking-Fehler, in dem alle zugehörigen Patches aufgeführt sind, enthalten weitere Informationen.
Vorschaukanäle herunterladen
Verwenden Sie Chrome Canary, Dev oder Beta als Standardbrowser für die Entwicklung. Über diese Preview-Channels haben Sie Zugriff auf die neuesten DevTools-Funktionen, können innovative Webplattform-APIs testen und Probleme auf Ihrer Website finden, bevor Ihre Nutzer sie entdecken.
Chrome-Entwicklertools-Team kontaktieren
Verwenden Sie die folgenden Optionen, um über die neuen Funktionen, Updates oder alles andere im Zusammenhang mit den Entwicklertools zu sprechen.
- Sie können uns Feedback und Funktionsanfragen unter crbug.com senden.
- Melden Sie ein DevTools-Problem in den Entwicklertools über das Weitere Optionen > Hilfe > DevTools-Problem melden.
- Senden Sie einen Tweet an @ChromeDevTools.
- Hinterlassen Sie Kommentare unter den YouTube-Videos zu Neuerungen in den DevTools oder den YouTube-Videos mit DevTools-Tipps.