Chroma Dark Mode ohne JavaScript
Dark Mode für Code-Blöcke, der komplett ohne JavaScript läuft
Seit Hugo 0.165 gibt es die Funktion css.ChromaStyles, die mit dem neuen Attribut mode ein vollständiges Chroma-Stylesheet erzeugt. Kombiniert mit dem media-Attribut der <link>-Tags lässt sich damit einen Dark-/Light-Wechsel herstellen – der Browser entscheidet allein über das Betriebssystem-Thema, ohne zusätzliches JavaScript.
Meine gesamte Seite stellt einen Dark-/Light-Wechsel bereit, der ohne JavaScript läuft. Deshalb suchte ich nach einer Lösung, die sich in mein bestehendes Design integrieren lässt.
Code-Blöcke auf dieser Seite werden mit Chroma farbig markiert. Die klassische Dark-Mode-Lösung dafür braucht JavaScript: Man generiert ein Dark-Stylesheet, dessen Selektoren unter einer dark-Klasse gescoped sind, und schaltet dann per Skript das class-Attribut am <html>-Element um – gern zusätzlich mit einem manuellen Light/Dark-System-Schalter. Dieses Vorgehen ist in Chromastyles beschrieben.
Voraussetzungen
Hugo muss die Code-Blöcke mit CSS-Klassen markieren statt inline Styles zu schreiben, und der gewählte Style muss beide Modi unterstützen. github z.B. unterstützt das:
markup:
highlight:
noClasses: false
style: github
Zwei Stylesheets, zwei Media Queries
Das Herzstück ist das Partial _partials/css.html. Es erzeugt aus allen Stylesheet-Resource eine einzige CSS-Datei. Zusätzlich wird jetzt für jeden Modus ein eigenes Chroma-Stylesheet erzeugt, publiziert und mit passendem Media Query eingebunden:
{{ $light := css.ChromaStyles (dict "targetPath" "css/light.css" "mode" "light") }}
{{ with $light | css.Build | resources.Publish }}
<link rel="stylesheet" href="{{ .RelPermalink }}"
media="screen and (prefers-color-scheme: light)" />
{{ end }}
{{ $dark := css.ChromaStyles (dict "targetPath" "css/dark.css" "mode" "dark") }}
{{ with $dark | css.Build | resources.Publish }}
<link rel="stylesheet" href="{{ .RelPermalink }}"
media="screen and (prefers-color-scheme: dark)" />
{{ end }}
In Produktion werden beide Links zusätzlich über fingerprint mit integrity und crossorigin="anonymous" versehen, in der Entwicklung entfällt das – dort läuft css.Build ohne Minifizierung und mit sourcemap.
Warum kein JavaScript nötig ist
Das media-Attribut eines <link>-Tags entscheidet, ob das Stylesheet zum Tragen kommt: Der Browser wendet nur das Sheet an, dessen Media Query zutrifft – moderne Browser laden nichtzutreffende Sheets gar nicht erst. Da jedes der beiden Files einen vollständigen Farbmodus enthält, greift genau eines.
Der Wechsel folgt damit automatisch dem OS- bzw. Browser-Thema, ohne dass ein class-Attribut gesetzt oder ein Event Listener auf das matchMedia-Event nötig wäre. Es gibt keinen Flash of unstyled Content, weil die zutreffende Farbdefinition schon vor dem Rendern verfügbar ist.
Damit folgt das Theme ausschließlich der Systemeinstellung. Ein manueller Schalter pro Seite ist so nicht nötig.
Der Rest der Seite schaltet mit
Für alle Farben außerhalb der Code-Blöcke genügt die CSS-Funktion light-dark(). Das Theme setzt sie über CSS Custom Properties am <html>-Element an:
html {
color-scheme: light dark;
--cfg: light-dark(#222831, #cdd9e5);
--cbg: light-dark(#eeeeee, #202020);
}
color-scheme meldet dem Browser beide Schemata an (u. a. für Formulare und Scrollbars), light-dark() liefert pro Variable automatisch den zum aktuellen Schema passenden Wert. Auch hier entscheidet also das Betriebssystem – kein JavaScript nötig.
Randfall Drucker
Beide Chroma-Sheets sind auf screen beschränkt, daher wird beim Ausdrucken keines von beiden angewendet und Code-Blöcke erscheinen farbeingefasst in Schwarz-Weiss. Wer auch fürs Papier highlighten will, bindet zusätzlich ein drittes Sheet mit media="print" ein.