mitmario.dev

Dunkelmodus und jeder Bildschirm

CSS Im Browser 3 Min Lesezeit 3 BeispieleLektion 6 von 7

Ein Dunkelmodus und ein Layout von 320px bis Desktop haben scheinbar nichts miteinander zu tun. Beide machen aber fast keine Arbeit mehr, wenn die Vorarbeit stimmt, und hier kommen sie zusammen.

Warum die Tokens Rollen tragen

Ein Token, zwei Werte
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 16px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  background-color: #a1a1aa;
}

.titel {
  margin-bottom: 4px;
  font-size: 14px;
  color: #18181b;
}

/* Jedes Token trägt beide Werte. Welcher gilt,
   entscheidet das color-scheme des Elements. Im echten
   Projekt steht color-scheme: light dark am :root und
   die Systemeinstellung wählt aus. Hier stehen zwei
   feste Werte nebeneinander, damit man beides sieht. */
.huelle {
  --farbe-text: light-dark(#18181b, #fafafa);
  --farbe-gedaempft: light-dark(#52525b, #a1a1aa);
  --farbe-flaeche: light-dark(#ffffff, #101012);
  --farbe-akzent: light-dark(#2563eb, #60a5fa);

  margin-bottom: 24px;
  padding: 16px;
  border-radius: 12px;
  background-color: light-dark(#f4f4f5, #18181b);
}

.hell {
  color-scheme: light;
}

.dunkel {
  color-scheme: dark;
}

/* Und hier steht jede Regel genau einmal. Sie liest
   nur Tokens und weiß von keinem Schema. */
.karte {
  padding: 16px;
  border-radius: 8px;
  color: var(--farbe-text);
  background-color: var(--farbe-flaeche);
}

.karte h3 {
  margin: 0 0 4px 0;
}

.karte p {
  margin: 0 0 16px 0;
  color: var(--farbe-gedaempft);
}

.button {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 8px;
  font-weight: 700;
  text-decoration: none;
  color: var(--farbe-flaeche);
  background-color: var(--farbe-akzent);
}

In Lektion 5.5 haben die Farben Rollennamen bekommen, --farbe-text, --farbe-gedaempft, --farbe-flaeche und --farbe-linie. Damals wirkte das wie eine Geschmacksfrage. Hier zahlt es sich aus.

Eine Rolle bleibt im Dunkelmodus dieselbe. Text ist weiterhin Text und die Fläche weiterhin die Fläche, nur der konkrete Wert dreht sich um. Du änderst die Tokens, und jede Regel, die sie liest, stimmt weiter.

Hieße das Token --grau-900, wäre es im Dunkelmodus das Hellste im Bild. Dann müsstest du jede Regel anfassen, die es benutzt, und jedes Mal entscheiden, ob dort Text oder Fläche gemeint war. Das ist der Unterschied zwischen einer Handvoll Zeilen und einer Woche.

light-dark()

light-dark(#ffffff, #101012) nimmt zwei Werte und wählt nach dem geltenden Farbschema einen davon aus. Erst kommt der helle, dann der dunkle. Alle großen Browser können das seit Mai 2024, Safari als letzter mit Version 17.5.

Damit trägt jedes Token beide Fassungen in einer Zeile, und es gibt keinen zweiten Block, den irgendwer an anderer Stelle mitpflegen müsste:

--farbe-flaeche: light-dark(#ffffff, #101012);

Voraussetzung ist, dass am Element ein Farbschema gilt. Das setzt color-scheme.

Was color-scheme allein schon bewirkt

Was color-scheme allein schon tut
body {
  margin: 0;
  padding: 16px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  background-color: #a1a1aa;
}

.titel {
  margin-bottom: 4px;
  font-size: 14px;
  color: #18181b;
}

/* An den Formularelementen steht keine einzige
   Farbregel. Das Feld, die Auswahl, das Häkchen und
   der Scrollbalken richten sich allein nach dem
   color-scheme. Das ist Arbeit, die du nicht machen
   musst und auch nicht besser machen könntest. */
.feld {
  display: grid;
  gap: 8px;
  margin-bottom: 24px;
  padding: 16px;
  border-radius: 12px;
}

.hell {
  color-scheme: light;
}

.dunkel {
  color-scheme: dark;
}

.feld p {
  margin: 0;
}

color-scheme: light dark am Wurzelelement sagt dem Browser, dass die Seite beides kann. Er nimmt dann, was der Nutzer eingestellt hat.

Diese eine Zeile richtet ohne jede weitere Regel eine ganze Reihe von Dingen mit.

  • die Standardfarben für Text und Hintergrund,
  • alle Formularelemente, also Felder, Auswahllisten, Häkchen und Schalter,
  • die Scrollbalken,
  • die Fokusringe des Browsers.

Im Beispiel steht an den Formularelementen keine einzige Farbregel, und trotzdem sehen sie in beiden Schemata richtig aus. Diese Arbeit musst du nicht machen, und von Hand bekämst du sie auch nicht besser hin, weil jedes Betriebssystem seine Felder, Häkchen und Scrollbalken ein bisschen anders zeichnet.

Drehst du nur die eigenen Farben um und lässt color-scheme weg, bekommst du eine dunkle Seite mit weißen Eingabefeldern und einem weißen Scrollbalken.

Brauchst du einen eigenen Umschalter statt der Systemeinstellung, führt der Weg über prefers-color-scheme und überschriebene Tokens. Die dunklen Werte stehen dann in einem @media (prefers-color-scheme: dark)-Block, und eine Klasse am html kann das übersteuern. Das ist mehr Arbeit und nur nötig, wenn der Nutzer wirklich wählen können soll.

Dunkelmodus heißt nicht invertieren

Nicht einfach invertieren
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 16px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  background-color: #52525b;
}

.titel {
  margin-bottom: 4px;
  font-size: 14px;
  color: #fafafa;
}

.huelle {
  margin-bottom: 24px;
  padding: 16px;
  border-radius: 12px;
}

.karte {
  padding: 16px;
  border-radius: 8px;
}

.karte h3 {
  margin: 0 0 4px 0;
}

.karte p {
  margin: 0;
}

/* Ein Schatten ist dunkler Grund auf hellem Grund.
   Auf schwarzem Grund gibt es nichts mehr, das dunkler
   wäre, also ist er unsichtbar. */
.stumpf {
  background-color: #000000;
}

.stumpf .karte {
  color: #ffffff;
  background-color: #000000;
  box-shadow: 0 8px 24px rgb(0 0 0 / 40%);
}

/* Gebrochene Töne statt der Extreme, und die Trennung
   übernimmt eine helle Kante. */
.abgestimmt {
  background-color: #18181b;
}

.abgestimmt .karte {
  color: #fafafa;
  background-color: #101012;
  border: 1px solid rgb(255 255 255 / 12%);
}

Drei Dinge gehen kaputt, wenn man die Farben einfach umdreht.

Schatten funktionieren nicht mehr. Ein Schatten ist eine dunklere Fläche auf einer helleren. Auf schwarzem Grund gibt es nichts Dunkleres, er ist schlicht unsichtbar. Im Dunkeln trennt deshalb eine helle Kante die beiden Ebenen, also ein Rahmen mit wenigen Prozent Weiß.

Reines Weiß auf reinem Schwarz blendet. Der Kontrast ist maximal, und bei längerem Lesen flimmert der Text. Ich nehme gebrochene Töne, im Beispiel #fafafa auf #101012. Der Kontrast liegt dann immer noch weit über den geforderten 4,5 zu 1.

Bilder wirken zu grell. Ein helles Foto auf dunklem Grund sticht heraus, und ein leichtes filter: brightness(0.9) im Dunkelmodus nimmt das raus. Bei Fotos von Menschen und bei Produktbildern lasse ich allerdings die Finger davon.

Das Layout für jede Breite

Der zweite Teil kommt aus den ersten beiden Lektionen dieses Abschnitts. Die Grundform gilt für schmal, und eine Media Query legt die breite Fassung obendrauf.

Beides zusammen ergibt die Seite, an der du seit Abschnitt 4 baust. In der Aufgabe kennen ihre Farb-Tokens nur das helle Schema, und die schmale Fassung gilt auf jeder Breite.

Zum Mitnehmen

Tragen die Tokens Rollen, schaltet light-dark() in jedem Token beide Werte, und color-scheme: light dark am Wurzelelement richtet Formularelemente und Scrollbalken mit. Die Breiten kommen wie in 12.2 obendrauf.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.