Neulich fragte Florens Verschelde, wie man Dark-Mode-Stile sowohl für eine Klasse als auch für eine Media Query definiert, ohne CSS-Custom-Properties-Deklarationen zu wiederholen. Dieses Problem hatte ich in der Vergangenheit auch, aber noch keine richtige Lösung gefunden.
Was wir wollen, ist die erneute Definition – und damit Wiederholung – von Custom Properties beim Wechsel zwischen hellem und dunklem Modus zu vermeiden. Das ist das Ziel der DRY (Don't Repeat Yourself)-Programmierung, aber das typische Muster für den Wechsel von Themes sieht normalerweise so aus:
:root {
--background: #fff;
--text-color: #0f1031;
/* etc. */
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0f1031;
--text-color: #fff;
/* etc. */
}
}
Sehen Sie, was ich meine? Sicher, in einem verkürzten Beispiel wie diesem mag es nicht nach viel aussehen, aber stellen Sie sich vor, Sie jonglieren gleichzeitig mit Dutzenden von Custom Properties – das ist viel Duplizierung!
Dann fiel mir Leas Verous Trick mit --var: ; ein, und obwohl es mir zunächst nicht einfiel, fand ich einen Weg, ihn zum Laufen zu bringen: nicht mit var(--light-value, var(--dark-value)) oder einer verschachtelten Kombination wie dieser, sondern indem ich beide nebeneinander verwende!
Sicher, jemand Schlaueres muss das schon vor mir entdeckt haben, aber ich habe noch nicht davon gehört, CSS-Custom-Properties zu nutzen (oder besser gesagt, zu missbrauchen), um dies zu erreichen. Ohne weitere Umschweife, hier ist die Idee:
--color: var(--light, orchid) var(--dark, rebeccapurple);
Wenn der Wert von --light auf initial gesetzt ist, wird der Fallback verwendet (orchid), was bedeutet, dass --dark auf ein Leerzeichen gesetzt werden sollte (was ein gültiger Wert ist), wodurch der endgültig berechnete Wert so aussieht:
--color: orchid ; /* Note the additional whitespace */
Umgekehrt, wenn --light auf ein Leerzeichen und --dark auf initial gesetzt ist, erhalten wir einen berechneten Wert von:
--color: rebeccapurple; /* Again, note the whitespace */
Das ist großartig, aber wir müssen die --light- und --dark-Custom-Properties basierend auf dem Kontext definieren. Der Benutzer kann eine Systemeinstellung haben (entweder hell oder dunkel) oder das Theme der Website über ein UI-Element umgeschaltet haben. Genau wie in Florens' Beispiel definieren wir diese drei Fälle mit einigen geringfügigen Lesbarkeitsverbesserungen, die Lea vorgeschlagen hat, indem sie „on“- und „off“-Konstanten verwendet, um sie auf einen Blick leichter verständlich zu machen:
:root {
/* Thanks Lea Verou! */
--ON: initial;
--OFF: ;
}
/* Light theme is on by default */
.theme-default,
.theme-light {
--light: var(--ON);
--dark: var(--OFF);
}
/* Dark theme is off by default */
.theme-dark {
--light: var(--OFF);
--dark: var(--ON);
}
/* If user prefers dark, then that's what they'll get */
@media (prefers-color-scheme: dark) {
.theme-default {
--light: var(--OFF);
--dark: var(--ON);
}
}
Wir können dann alle unsere Theme-Variablen in einer einzigen Deklaration ohne Wiederholung einrichten. In diesem Beispiel sind die theme-*-Klassen auf das html-Element gesetzt, sodass wir :root als Selektor verwenden können, wie viele Leute es gerne tun, aber Sie könnten sie auf das body setzen, wenn die kaskadierende Natur der Custom Properties auf diese Weise sinnvoller ist.
:root {
--text: var(--light, black) var(--dark, white);
--bg: var(--light, orchid) var(--dark, rebeccapurple);
}
Und um sie zu verwenden, verwenden wir var() mit integrierten Fallbacks, weil wir gerne vorsichtig sind:
body {
color: var(--text, navy);
background-color: var(--bg, lightgray);
}
Hoffentlich sehen Sie hier schon den Vorteil. Anstatt Armladungen von Custom Properties zu definieren und zu wechseln, beschäftigen wir uns mit zwei und setzen alle anderen nur einmal auf :root. Das ist eine enorme Verbesserung gegenüber dem, wo wir angefangen haben.
Noch DRYer mit Präprozessoren
Wenn Sie mir diese folgende Codezeile außerhalb des Kontexts zeigen würden, wäre ich sicherlich verwirrt, denn eine Farbe ist ein einzelner Wert, nicht zwei!
--text: var(--light, black) var(--dark, white);
Deshalb ziehe ich es vor, die Dinge etwas zu abstrahieren. Wir können eine Funktion mit unserem bevorzugten Präprozessor einrichten, der in meinem Fall Sass ist. Wenn wir unseren obigen Code beibehalten, der unsere --light- und --dark-Werte in verschiedenen Kontexten definiert, müssen wir nur die tatsächliche Custom-Property-Deklaration ändern. Erstellen wir eine light-dark-Funktion, die die CSS-Syntax für uns zurückgibt:
@function light-dark($light, $dark) {
@return var(--light, #{ $light }) var(--dark, #{ $dark });
}
Und wir würden sie so verwenden:
:root {
--text: #{ light-dark(black, white) };
--bg: #{ light-dark(orchid, rebeccapurple) };
--accent: #{ light-dark(#6d386b, #b399cc) };
}
Sie werden bemerken, dass Interpolationsbegrenzer #{ … } um den Funktionsaufruf herum vorhanden sind. Ohne diese würde Sass den Code so ausgeben, wie er ist (wie eine Vanilla-CSS-Funktion). Sie können mit verschiedenen Implementierungen davon experimentieren, aber die Komplexität der Syntax liegt in Ihrem Geschmack.
Wie wäre das für eine viel DRYere Codebasis?
Mehr als ein Theme? Kein Problem!
Man könnte das potenziell mit mehr als zwei Modi machen. Je mehr Themes Sie hinzufügen, desto komplexer wird die Verwaltung, aber der Punkt ist, dass es *möglich* ist! Wir fügen ein weiteres Theme-Set von ON- oder OFF-Variablen hinzu und setzen eine zusätzliche Variable in der Liste der Werte.
.theme-pride {
--light: var(--OFF);
--dark: var(--OFF);
--pride: var(--ON);
}
:root {
--text:
var(--light, black)
var(--dark, white)
var(--pride, #ff8c00)
; /* Line breaks are absolutely valid */
/* Other variables to declare… */
}
Ist das ein Hack? Ja, absolut. Ist das ein großartiger Anwendungsfall für potenzielle, noch nicht existierende CSS-Booleans? Nun, das ist der Traum.
Wie sieht es bei Ihnen aus? Haben Sie das mit einem anderen Ansatz herausgefunden? Teilen Sie es in den Kommentaren!
Hey, tolle Hacks! Ich liebe es.
Ich frage mich, ob es eine Sass-Funktion gibt, die das für Sie erledigt? Scheint möglich. Danke fürs Posten!
Aber sicher! Sie können einen Präprozessor verwenden, um die unübersichtliche Syntax für Sie zu handhaben, siehe diesen Abschnitt dieses Artikels. Oder treiben Sie die Automatisierung mit einigen Theme-Tokens weiter voran: https://codepen.io/chriskirknielsen/pen/QWGaXqP
Das fühlt sich für mich wie ein Anti-Pattern an. Wenn Sie zusätzliche Themes hinzufügen möchten, müssen Sie immer die Basisdefinitionen ändern und das neue Theme einmischen. Ich bevorzuge den Ansatz, bei dem ein Theme einfach die Eigenschaften überschreibt, die es überschreiben muss. Auf diese Weise können Sie jedes Theme in einer separaten Datei haben, wenn Sie einen Präprozessor verwenden. Das Hinzufügen oder Entfernen eines Themes ist nur ein Import und nicht das Durchgehen aller Eigenschaften einzeln.
Das ist definitiv ein Hack und keine Best Practice. Es gehört eher in den Bereich der Wissenschaftler aus Jurassic Park, die darüber nachdachten, was sie tun könnten, nicht was sie tun sollten. ;)
Wenn Sie die Themes häufig ändern müssen und diese Methode wirklich nutzen möchten, dann ist es vielleicht besser, dies in einem Präprozessor mit einer komplexeren Funktion, die eine Map von Tokens für jedes Theme liest, etwas zu abstrahieren. Sehen Sie sich diese Demo an, um eine grobe Vorstellung davon zu bekommen, wie man das macht: https://codepen.io/chriskirknielsen/pen/QWGaXqP?editors=0100 Ich sage nicht, dass es der beste Weg ist, nur dass es gemacht werden kann. :)
Interessant! Wie oft würden Sie das oder eine ähnliche Lösung verwenden? Ich frage das aus der Perspektive von jemandem, der mehr im Backend mit viel React-Erfahrung versiert ist – ich würde wahrscheinlich einfach zwei separate Stylesheet-Sets haben und einen dynamischen Import durchführen. Ist das ab einem bestimmten Punkt nicht mehr nachhaltig?
Ich denke, das ist ein cooler Trick zu wissen, aber ehrlich gesagt kann ich nichts über die Anwendung in der Produktion sagen – es ist eher ein Experiment.
Sie könnten definitiv ein globales Stylesheet haben, das Ihre
--text,--bgusw. liest, zusammen mit einzelnen Stylesheets, um die Variablen zu deklarieren, und diese dann im laufenden Betrieb austauschen. Das funktioniert, solange Sie die alte Theme-Datei beibehalten, während die neue geladen wird, um eine Seite zu vermeiden, auf der alle Variablen fehlen.Wenn Sie ohnehin schon React verwenden, ist dieser Trick hier vielleicht nicht super nützlich. Ich bin auch nicht so gut in der React-Welt bewandert, daher gibt es vielleicht schon coole Tricks, um ein ähnliches Ergebnis zu erzielen, aber dies ist ein reiner CSS-Ansatz (wenn man das Umschalten der
theme-*-Klasse nicht mitzählt), über den ich gerne gesprochen hätte. :)Außer dass Sie jetzt bei jeder Deklaration wiederholen müssen, anstatt zweimal am Anfang der Datei.
Wenn ich Ihren Punkt nicht falsch verstanden habe, ist das nicht der Fall: Sobald Sie Ihre Variablen wie
--textoder--accentin:rootdeklariert haben, sind sie in Ihrem gesamten Stylesheet verfügbar, nichts muss erneut deklariert werden.Durch die Verwendung der Animation-Frame-Tricks für Custom Properties aus einem früheren Beitrag (https://css-tricks.de/css-switch-case-conditions/) habe ich festgestellt, dass man das noch DRYer machen könnte.
Das ist eine weitere Möglichkeit, es zu tun; was auch immer Ihnen gefällt! Es erfordert ein wenig mehr Überlegung mit den Prozentwerten, wenn Sie eine ungerade/große Anzahl von Themes haben, aber für die meisten Fälle mit einem hellen und einem dunklen Theme funktioniert Ihr Weg gut!
Ich erinnere mich, vor ein paar Jahren eine Art ähnlichen Trick verwendet zu haben, um responsive Schriftgrößen zu erzielen, aber es erforderte JS, um die negative Verzögerung basierend auf der Viewport-Größe anzupassen. Mit Custom Properties ist es viel einfacher!
Ich erkannte, dass man nicht die gesamte Animation nutzen musste, sodass man jeden Keyframe auf 1% abbilden und die Dauer auf 100s setzen konnte, was mehr als genug Spielraum für die meisten Anwendungen bietet. Meine Hauptsorge bei diesem reinen Keyframe-Animationsansatz ist, ob er signifikante Leistungskosten oder Barrierefreiheitsprobleme mit sich bringt. Ich weiß nicht genug über die Interna von Animationen, um zu wissen, ob dies einen massiven Speicherverbrauch verursacht oder durch Benutzereinstellungen beeinträchtigt werden könnte.
Ich habe das vor ein paar Monaten geschrieben. https://dev.to/drno/theming-and-coloring-finally-made-efficient-in-css-thanks-to-an-oop-inspired-pattern-27ca Verwende eine fortgeschrittenere Version dieses Musters bereits seit über einem Jahr in der Produktion. Ich würde gerne wissen, was Sie davon halten.
PS: V2 kommt in ein paar Tagen