Chroma Dark Mode ohne JavaScript
Dark Mode für Code-Blöcke, der komplett ohne JavaScript läuft
Seit Hugo 0.165 erzeugt die Funktion css.ChromaStyles mit dem Attribut mode vollständige Chroma-Stylesheets für einzelne Farbmodi. In Kombination mit dem media-Attribut der <link>-Tags lässt sich damit ein Dark-/Light-Wechsel implementieren, den der Browser allein anhand des Betriebssystem-Themas vornimmt – ohne zusätzliches JavaScript.
Die gesamte Seite wechselt zwischen den Farbmodi ohne JavaScript. Ziel war eine Lösung, die sich nahtlos in das bestehende Design integriert.
Code-Blöcke auf dieser Seite werden mit Chroma farbig markiert. Die übliche Dark-Mode-Lösung dafür setzt JavaScript ein: Es wird ein Dark-Stylesheet generiert, dessen Selektoren unter einer dark-Klasse liegen, und per Skript wird das class-Attribut am <html>-Element umgeschaltet – häufig zusätzlich mit einem manuellen Schalter. Dieses Vorgehen ist in Hugo Chromastyle Example beschrieben.
Voraussetzungen
Voraussetzung ist, dass Hugo die Code-Blöcke per CSS-Klassen markiert, statt inline-Stile einzubetten, und dass der gewählte Stil beide Farbmodi unterstützt. Der Stil github ist für beide Modi geeignet. Dazu müssen in der Config-Datei (config/_default/hugo.yaml) des Projekts die entsprechenden Einstellungen vorgenommen werden.
markup:
highlight:
noClasses: false
style: github
Zwei Stylesheets, zwei Media Queries
Das Herzstück ist das Partial _partials/css.html. Es bündelt alle Stylesheets über css.Build zu einer einzigen CSS-Datei. Zusätzlich wird für jeden Farbmodus 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 per fingerprint um das integrity-Attribut und crossorigin="anonymous" ergänzt; in der Entwicklung entfallen beide, da css.Build dort ohne Minifizierung und mit Sourcemap läuft.
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. Beim Ausdrucken greift daher keines von ihnen, und Code-Blöcke erscheinen ohne Farbhervorhebung. Wer auch für den Druck eine Highlighting-Farbgebung möchte, bindet ein drittes Sheet mit media="print" ein.