Schriftfarbe für unterschiedliche Hintergründe mit CSS wechseln

Avatar of Facundo Corradini
Facundo Corradini am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

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 color je 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.

Ein heller Hintergrund mit einem dunklen Rand, der auf dieser Hintergrundfarbe basiert.

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

  1. 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.
  2. 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 an 400deg=40deg, 480deg=120deg usw.

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.