Ein DRY-Ansatz für Farbthemen in CSS

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

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!