Hatten Sie schon einmal einen dieser Momente, in denen Sie dachten: „Das kann ich mit CSS machen!“, während Sie jemandem zusahen, wie er seine JavaScript-Muskeln spielen ließ? Genau dieses Gefühl hatte ich, als ich Dag-Inge Aas & Ida Aalen bei der CSSconf EU 2018 reden hörte.
Sie haben ihren Sitz in Norwegen, wo WCAG-Zugänglichkeit nicht nur eine gute Praxis ist, sondern gesetzlich vorgeschrieben ist (weiter so, Norwegen!). Als sie eine Funktion entwickelten, die eine vom Benutzer wählbare Farbgestaltung für ihr Hauptprodukt ermöglicht, standen sie vor einer Herausforderung: die automatische Anpassung der Schriftfarbe basierend auf der ausgewählten Hintergrundfarbe des Containers. Wenn der Hintergrund dunkel ist, wäre es ideal, einen weißen Text zu haben, um ihn WCAG-kontrastkonform zu halten. Aber was passiert, wenn stattdessen eine helle Hintergrundfarbe gewählt wird? Der Text ist sowohl unleserlich als auch nicht zugänglich.
Sie nutzten einen eleganten Ansatz und lösten das Problem mit dem "color" npm-Paket, indem sie bedingte Ränder hinzufügten und gleichzeitig automatisch eine sekundäre Farbe generierten.
Aber das ist eine JavaScript-Lösung. Hier ist meine reine CSS-Alternative.
Die Herausforderung
Hier sind die Kriterien, die ich mir gesetzt habe:
- Die Schriftfarbe
colorje nach Hintergrundfarbe auf Schwarz oder Weiß ändern - Die gleiche Logik auf Ränder anwenden, indem eine dunklere Variante der Basisfarbe des Hintergrunds verwendet wird, um die Sichtbarkeit von Schaltflächen zu verbessern, nur wenn der Hintergrund wirklich hell ist
- Automatisch eine sekundäre Farbe mit um 60° gedrehtem Farbton generieren

Arbeiten mit HSL-Farben und CSS-Variablen
Der einfachste Ansatz, der mir einfiel, beinhaltet die Verwendung von HSL-Farben. Wenn die Hintergrunddeklarationen als HSL mit jeder als CSS-benutzerdefinierten Eigenschaft definierten Parameter eingestellt werden, ermöglicht dies eine sehr einfache Methode, die Helligkeit zu bestimmen und sie als Basis für unsere bedingten Anweisungen zu verwenden.
:root {
--hue: 220;
--sat: 100;
--light: 81;
}
.btn {
background: hsl(var(--hue), calc(var(--sat) * 1%), calc(var(--light) * 1%));
}
Dies sollte es uns ermöglichen, den Hintergrund zur Laufzeit auf jede gewünschte Farbe umzuschalten, indem die Variablen geändert und eine Wenn-Dann-Anweisung ausgeführt wird, um die Vordergrundfarbe zu ändern.
Nur... wir haben keine Wenn-Dann-Anweisungen in CSS... oder doch?
Einführung von CSS-Bedingungsanweisungen
Seit der Einführung von CSS-Variablen gibt es auch bedingte Anweisungen dazu. Oder so ähnlich.
Dieser Trick basiert auf der Tatsache, dass einige CSS-Parameter auf einen minimalen und maximalen Wert begrenzt werden. Denken Sie zum Beispiel an Opazität. Der gültige Bereich ist 0 bis 1, daher halten wir ihn normalerweise dort. Wir können aber auch eine Opazität von 2, 3 oder 1000 deklarieren, und sie wird auf 1 begrenzt und entsprechend interpretiert. In ähnlicher Weise können wir sogar einen negativen Opazitätswert deklarieren und ihn auf 0 begrenzen lassen.
.something {
opacity: -2; /* resolves to 0, transparent */
opacity: -1; /* resolves to 0, transparent */
opacity: 2; /*resolves to 1, fully opaque */
opacity: 100; /* resolves to 1, fully opaque */
}
Anwendung des Tricks auf unsere Schriftfarbdeklaration
Der Helligkeitsparameter einer HSL-Farbdeklaration verhält sich ähnlich, wobei negative Werte auf 0 begrenzt werden (was zu Schwarz führt, unabhängig von Farbton und Sättigung) und Werte über 100% auf 100% begrenzt werden (was immer Weiß ist).
Wir können also die Farbe als HSL deklarieren, den gewünschten Schwellenwert vom Helligkeitsparameter abziehen und dann mit 100% multiplizieren, um sie zu zwingen, eines der Grenzwerte zu überschreiten (entweder unter Null oder über 100%). Da wir negative Ergebnisse für Weiß und positive Ergebnisse für Schwarz benötigen, müssen wir sie auch invertieren, indem wir das Ergebnis mit -1 multiplizieren.
:root {
--light: 80;
/* the threshold at which colors are considered "light." Range: integers from 0 to 100,
recommended 50 - 70 */
--threshold: 60;
}
.btn {
/* Any lightness value below the threshold will result in white, any above will result in black */
--switch: calc((var(--light) - var(--threshold)) * -100%);
color: hsl(0, 0%, var(--switch));
}
Betrachten wir diesen Codeausschnitt: Ausgehend von einer Helligkeit von 80 und unter Berücksichtigung eines Schwellenwerts von 60 ergibt die Subtraktion 20, die mit -100% multipliziert -2000% ergibt, was auf 0% begrenzt wird. Unser Hintergrund ist heller als der Schwellenwert, daher betrachten wir ihn als hell und wenden schwarzen Text an.
Wenn wir die Variable --light auf 20 gesetzt hätten, hätte die Subtraktion -40 ergeben, was mit -100% multipliziert 4000% ergibt, begrenzt auf 100%. Unsere helle Variable ist niedriger als der Schwellenwert, daher betrachten wir sie als "dunklen" Hintergrund und wenden weißen Text an, um einen hohen Kontrast zu gewährleisten.
Generieren eines bedingten Randes
Wenn der Hintergrund eines Elements zu hell wird, kann es leicht auf einem weißen Hintergrund verloren gehen. Wir könnten eine Schaltfläche haben und sie nicht einmal bemerken. Um eine bessere Benutzeroberfläche bei wirklich hellen Farben zu bieten, können wir einen Rand basierend auf der gleichen Hintergrundfarbe, nur dunkler, festlegen.

Um dies zu erreichen, können wir die gleiche Technik verwenden, aber sie auf den Alphakanal einer HSLA-Deklaration anwenden. Auf diese Weise können wir die Farbe nach Bedarf anpassen und sie dann entweder vollständig transparent oder vollständig opak machen.
:root {
/* (...) */
--light: 85;
--border-threshold: 80;
}
.btn {
/* sets the border-color as a 30% darker shade of the same color*/
--border-light: calc(var(--light) * 0.7%);
--border-alpha: calc((var(--light) - var(--border-threshold)) * 10);
border: .1em solid hsla(var(--hue), calc(var(--sat) * 1%), var(--border-light), var(--border-alpha));
}
Unter der Annahme eines Farbtons von 0 und einer Sättigung von 100% liefert der obige Code einen vollständig opak roten Rand mit 70% der ursprünglichen Helligkeit, wenn die Hintergrundhelligkeit über 80 liegt, oder einen vollständig transparenten Rand (und damit gar keinen Rand), wenn sie dunkler ist.
Einstellen der sekundären, um 60° gedrehten Farbtonfarbe
Wahrscheinlich die einfachste der Herausforderungen. Dafür gibt es zwei mögliche Ansätze
- filter: hue-rotate(60): Das ist das Erste, was einem in den Sinn kommt, aber es ist nicht die beste Lösung, da es die Farbe von Kindelementen beeinflussen würde. Bei Bedarf kann dies mit einer entgegengesetzten Drehung rückgängig gemacht werden.
- HSL-Farbton + 60: Die andere Option ist, unseren Farbton-Variable zu nehmen und 60 hinzuzufügen. Da der Farbton-Parameter kein Begrenzungsverhalten bei 360 hat, sondern stattdessen umbricht (wie jeder CSS-Typ
<angle>), sollte er ohne Probleme funktionieren. Denken Sie an400deg=40deg,480deg=120degusw.
In Anbetracht dessen können wir eine Modifikatorklasse für unsere sekundärfarbigen Elemente hinzufügen, die 60 zum Farbtonwert hinzufügt. Da selbstmodifizierende Variablen in CSS nicht verfügbar sind (d.h. es gibt kein --hue: calc(var(--hue) + 60)), können wir eine neue Hilfsvariable für unsere Farbtonmanipulation zu unserem Basisstil hinzufügen und sie in der Hintergrund- und Randdeklaration verwenden.
.btn {
/* (...) */
--h: var(--hue);
background: hsl(var(--h), calc(var(--sat) * 1%), calc(var(--light) * 1%));
border:.1em solid hsla(var(--h), calc(var(--sat) * 1%), var(--border-light), var(--border-alpha));
}
Deklarieren Sie dann dessen Wert in unserem Modifikator neu
.btn--secondary {
--h: calc(var(--hue) + 60);
}
Das Beste an diesem Ansatz ist, dass er automatisch alle unsere Berechnungen an den neuen Farbton anpasst und sie aufgrund der Gültigkeit von CSS-Benutzerdefinierten Eigenschaften auf die Eigenschaften anwendet.
Und da haben wir es. Wenn wir alles zusammenfügen, hier ist meine reine CSS-Lösung für alle drei Herausforderungen. Dies sollte einwandfrei funktionieren und uns die Einbindung einer externen JavaScript-Bibliothek ersparen.
Sehen Sie den Pen CSS Automatische Schriftfarbenumschaltung je nach Elementhintergrund…. FEHLGESCHLAGEN von Facundo Corradini (@facundocorradini) auf CodePen.
Außer dass es das nicht tut. Einige Farbtöne werden sehr problematisch (insbesondere Gelb und Cyan), da sie wesentlich heller angezeigt werden als andere (z.B. Rot und Blau), obwohl sie den gleichen Helligkeitswert haben. Folglich werden einige Farben als dunkel behandelt und erhalten weißen Text, obwohl sie extrem hell sind.
Was in aller Welt geschieht hier in CSS?
Einführung der wahrgenommenen Helligkeit
Ich bin sicher, viele von Ihnen haben es schon längst bemerkt, aber für den Rest von uns: Es stellt sich heraus, dass die Helligkeit, die wir wahrnehmen, nicht dieselbe ist wie die HSL-Helligkeit. Glücklicherweise haben wir einige Methoden, um die Helligkeit des Farbtons zu berücksichtigen und unseren Code so anzupassen, dass er auch auf den Farbton reagiert.
Dazu müssen wir die wahrgenommene Helligkeit der drei Grundfarben berücksichtigen, indem wir jeder einen Koeffizienten zuweisen, der der Helligkeit entspricht, mit der das menschliche Auge sie wahrnimmt. Dies wird üblicherweise als Luma bezeichnet.
Es gibt mehrere Methoden, um dies zu erreichen. Einige sind in bestimmten Fällen besser als andere, aber keine ist zu 100% perfekt. Daher habe ich die beiden beliebtesten ausgewählt, die gut genug sind
- sRGB-Luma (ITU Rec. 709): L = (rot * 0.2126 + grün * 0.7152 + blau * 0.0722) / 255
- W3C-Methode (Entwurf): L = (rot * 0.299 + grün * 0.587 + blau * 0.114) / 255
Implementierung von Luma-korrigierten Berechnungen
Die erste offensichtliche Konsequenz der Verwendung eines Luma-korrigierten Ansatzes ist, dass wir HSL nicht verwenden können, da CSS keine nativen Methoden zum Zugriff auf die RGB-Werte einer HSL-Deklaration bietet.
Wir müssen also auf eine RGB-Deklaration für die Hintergründe umsteigen, die Luma mit der gewählten Methode berechnen und sie für unsere Vordergrundfarbdeklaration verwenden, die HSL sein kann (und sein wird).
:root {
/* theme color variables to use in RGB declarations */
--red: 200;
--green: 60;
--blue: 255;
/* the threshold at which colors are considered "light".
Range: decimals from 0 to 1, recommended 0.5 - 0.6 */
--threshold: 0.5;
/* the threshold at which a darker border will be applied.
Range: decimals from 0 to 1, recommended 0.8+ */
--border-threshold: 0.8;
}
.btn {
/* sets the background for the base class */
background: rgb(var(--red), var(--green), var(--blue));
/* calculates perceived lightness using the sRGB Luma method
Luma = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255 */
--r: calc(var(--red) * 0.2126);
--g: calc(var(--green) * 0.7152);
--b: calc(var(--blue) * 0.0722);
--sum: calc(var(--r) + var(--g) + var(--b));
--perceived-lightness: calc(var(--sum) / 255);
/* shows either white or black color depending on perceived darkness */
color: hsl(0, 0%, calc((var(--perceived-lightness) - var(--threshold)) * -10000000%));
}
Für die bedingten Ränder müssen wir die Deklaration in RGBA umwandeln und wieder den Alphakanal verwenden, um ihn entweder vollständig transparent oder vollständig opak zu machen. So ziemlich dasselbe wie zuvor, nur mit RGBA statt HSLA. Der dunklere Farbton wird durch Subtraktion von 50 von jedem Kanal erhalten.
Es gibt einen wirklich seltsamen Fehler in WebKit auf iOS, der anstelle der entsprechenden Farbe einen schwarzen Rand anzeigt, wenn Variablen in den RGBA-Parametern der Kurzform-Randdeklaration verwendet werden. Die Lösung ist, den Rand in Langform zu deklarieren.
Danke Joel für den Hinweis auf den Fehler!
.btn {
/* (...) */
/* applies a darker border if the lightness is higher than the border threshold */
--border-alpha: calc((var(--perceived-lightness) - var(--border-threshold)) * 100);
border-width: .2em;
border-style: solid;
border-color: rgba(calc(var(--red) - 50), calc(var(--green) - 50), calc(var(--blue) - 50), var(--border-alpha));
}
Da wir unsere ursprüngliche HSL-Hintergrunddeklaration verloren haben, muss unser sekundärer Themton über eine Farbtonrotation erhalten werden
.btn--secondary {
filter: hue-rotate(60deg);
}
Das ist nicht das Allerbeste. Neben der Anwendung der Farbtonrotation auf potenzielle Kindelemente, wie bereits diskutiert, bedeutet dies, dass die Umschaltung auf Schwarz/Weiß und die Sichtbarkeit des Randes beim sekundären Element vom Farbton des Hauptelements und nicht vom eigenen abhängen. Soweit ich sehen kann, hat die JavaScript-Implementierung aber das gleiche Problem, also nenne ich es gut genug.
Und da haben wir es, diesmal für immer.
Sehen Sie den Pen CSS Automatische WCAG-Kontrast-Schriftfarbe je nach Elementhintergrund von Facundo Corradini (@facundocorradini) auf CodePen.
Eine reine CSS-Lösung, die denselben Effekt wie der ursprüngliche JavaScript-Ansatz erzielt, aber den Fußabdruck erheblich reduziert.
Browser-Unterstützung
IE ist aufgrund der Verwendung von CSS-Variablen ausgeschlossen. Edge hat nicht das Begrenzungsverhalten, das wir durchgehend verwendet haben. Es erkennt die Deklaration, hält sie für Unsinn und verwirft sie vollständig, wie jede fehlerhafte/unbekannte Regel. Alle anderen wichtigen Browser sollten funktionieren.
Das ist fantastisch, etwas, wonach ich lange gesucht habe. Danke Facundo fürs Teilen.
Danke! Gerne geschehen :)
Unglaublich, einfach unglaublich.
Wow, toller Beitrag über die Arbeit mit Farben in CSS.
Brillante Arbeit.
Ich stelle fest, dass unter iOS der Rand immer schwarz (oder transparent schwarz) ist, also scheint WebKit irgendwo falsch zu liegen.
Hallo Joel! Danke für den Hinweis. Ich werde es mir genau ansehen und sehen, ob ich eine Lösung finden kann. Anscheinend werden die RGB-Werte der RGBA-Deklaration nicht übernommen. Vielleicht liefert die Division Dezimalwerte und WebKit braucht Ganzzahlen?… Ich werde das prüfen und melde mich wieder :)
Es stellte sich heraus, dass es ein wirklich seltsamer Fehler war, anscheinend akzeptiert WebKit keine Variablen in der RGBA-Deklaration, wenn sie in der Kurzform des Randes verwendet werden, plus die Ganzzahlen vs. Dezimalzahlen, an die ich zuerst gedacht hatte.
Jedenfalls ist die Lösung, die Randdeklaration in Langform zu ändern und den dunkleren Farbton durch eine Subtraktion zu erzeugen.
Danke für den Input! Ich werde den Beitrag mit der geänderten Deklaration aktualisieren.
Sicherlich lustig zu sehen, ich kann kaum einen Fall erkennen, in dem das nützlich ist.
Glauben Sie es oder nicht, da stimme ich Ihnen zu. Es ist eher eine Idee, die die Grenzen austestet, als etwas, das wir tatsächlich in der Produktion verwenden würden. Dennoch gibt es beim Entwickeln oder Lesen solcher Beiträge immer etwas mitzunehmen. Vielleicht können wir die Begrenzung der Opazität und der Alpha-Werte für andere Berechnungen verwenden, vielleicht hat der bedingte Rand einen gewissen Wert.
Diese spezielle Implementierung dient nur als Live-Vorschau beim Erstellen einer benutzerdefinierten Vorlage für ein Produkt, aber wir könnten genauso gut alle diese seltsamen Berechnungen auf der Wurzel einstellen und dann die Hintergründe unserer Elemente färben, indem wir die Werte –r, –g und –b ändern, was unsere gesamten Websites automatisch die Vordergrundfarbe an den Hintergrund anpassen lassen sollte. Das könnte nützlich sein, wenn eine Komponente in verschiedene Umgebungen eingefügt wird oder wenn eine Zustandsänderung den Hintergrund wechseln lässt.
Großartige Arbeit, A+.
Danke Kellen!
Brillant, aber was ist mit farbenblinden Benutzern? Die wahrgenommene Leuchtdichte könnte für sie immer noch ein Problem sein.
Danke Chris! Das ist eine berechtigte Sorge, besonders angesichts der Tatsache, dass die häufigste Form der Farbenblindheit Rot-Grün ist und diese bei der Berechnung der wahrgenommenen Helligkeit ganz unterschiedliche Gewichte haben.
Ich muss zugeben, dass ich die Antwort nicht wirklich kenne, obwohl ich ziemlich viele Stunden investiert habe, um dieses spezielle Problem zu recherchieren, als ich den Artikel geschrieben habe.
Wenn Sie eine gute Quelle dafür haben, wie Farbenblindheit mit wahrgenommener Helligkeit zusammenhängt, werde ich sie mir gerne ansehen.
Leider konnte ich keine Veröffentlichungen zu diesem Thema finden. Ich habe jedoch die Demo mit der Chrome-Erweiterung "MonoChrome" (https://chrome.google.com/webstore/detail/monochrome/nahpfoghefabhigpjfhgliafalpejclf) aktiviert und es scheint in Ordnung zu sein.
In der letzten Codepen-Demo. Nur die Änderung von Grün beeinflusst die Schriftfarbe, ist das Zufall?
Nein, das ist einfach die Funktionsweise von RGB-Farben. Der Hintergrund wird ohne Beimischung von Grün einfach nicht hell genug. Es ist ein ziemlich seltsames und unintuitives Farbsystem.
Vielleicht lassen Sie die Anfangswerte Sie das vermuten, aber setzen Sie das Grün auf einen höheren Wert und ändern Sie dann Rot oder Blau, und Sie werden sehen, dass der Text auch auf diese Änderungen reagiert.
Das ist eines der coolsten Dinge, die ich je über CSS-Variablen und ihre tatsächliche Verwendung gelesen habe! Sehr erhellend. Danke fürs Teilen.
Danke! CSS-Variablen sind wirklich großartig. Diese Technik ist so ziemlich ein Hack, aber der kürzlich veröffentlichte öffentliche Entwurf von CSS Values and Units Module Level 4 führt die mathematischen Ausdrücke min(), max() und clamp() ein, sodass wir, sobald die Browser sie implementieren, diese Art von Berechnungen überall verwenden können, auf eine nicht so hacky Weise :)
Würde das Folgende nicht funktionieren
Hallo Anthony,
Das ist ein ziemlich cleverer Ansatz und funktioniert sicherlich für einige Fälle, wobei die Haupteinschränkungen darin liegen, wie aufdringlich es für die Markup-Struktur wäre. Manchmal wünschte ich mir wirklich, wir könnten den Textknoten eines Elements direkt ansprechen...
Dem Kontrastfilter fehlt übrigens ein weiteres Nul, sonst erzeugt er auf einigen Farben Grautöne (z.B. #F60).