Gå til innholdet

La brukeren velge tema

Fargene følger maskinens innstilling uten at du gjør noe. Vil brukeren overstyre den, er selve byttet ren CSS: fs-theme-control på en radioknapp gjør value til et temavalg. Å få valget til å vare er noe annet, og det er appens oppgave.

Åpne temavelgeren i egen fane (der virker Tab og hopplenken som på en ekte side)

Gruppa er et vanlig <fieldset> med radioknapper, altså toggle-gruppa. Klassen står på hver knapp, ikke på gruppa.

<fieldset class="fs-toggle-group">
<legend class="fs-sr-only">Tema</legend>
<label class="fs-toggle-group__option">
<input class="fs-theme-control" type="radio" name="tema"
value="auto" checked /> Følg systemet
</label>
<label class="fs-toggle-group__option">
<input class="fs-theme-control" type="radio" name="tema"
value="light" /> Lyst
</label>
<label class="fs-toggle-group__option">
<input class="fs-theme-control" type="radio" name="tema"
value="dark" /> Mørkt
</label>
</fieldset>

Tre verdier betyr noe: auto, light og dark. «Følg systemet» har ingen temablokk, og det er med vilje: en verdi uten blokk treffer ingenting, så maskinens innstilling gjelder igjen.

light og dark vinner over data-theme på <html>. Serveren kan altså sende det valget den har lagret, mens et klikk likevel slår igjennom før svaret er tilbake.

auto gjør det ikke: regelen for den melder seg eksplisitt ut når <html> bærer et tema, slik at serverens verdi står. Uten det ville :has() slått attributtet på spesifisitet. Skal «Følg systemet» virke med én gang, må det som lagrer valget også fjerne attributtet. Begge oppskriftene under gjør det.

Valget styrer to ting, og de er ikke det samme. Fristils farger kommer fra tokenene. Nettleserens egne flater, altså rullefelt, nedtrekkslista i en <select> og kalenderpanelet i et datofelt, styres av CSS-egenskapen color-scheme.

valueFristils fargercolor-scheme
autoingen overstyring: mediespørringen, eller data-theme hvis det står derlight dark, eller data-theme sitt
lightlyse tokenslight
darkmørke tokensdark

light dark er ikke et tema. Det er en beskjed til nettleseren om at siden fungerer i begge, så den kan tegne sine egne flater etter systeminnstillingen. light og dark låser den til ett.

Du skal ikke sette color-scheme selv når du bruker velgeren. Se siste punkt nederst.

CSS kan bytte tema, men ikke huske det. Tre veier, med ulike krav:

VeiKreverVarer over sidelasting
Bare kontrolleningentingnei
Cookie, lest på serverenen server som rendrer HTMLja
localStorageJavaScriptja

Ingen av dem krever et rammeverk.

Dette er veien for en app som rendrer HTML på en server, uansett hvilket språk serveren er skrevet i. Gruppa står i et vanlig skjema, og hele rundturen er tre steg uten en linje JavaScript:

POST /tema tema=dark
→ Set-Cookie: tema=dark; Path=/; Max-Age=31536000; SameSite=Lax
→ 303 See Other
GET /soknad Cookie: tema=dark
→ <html data-theme="dark">

Serveren leser headeren og skriver attributtet. I Astro:

---
const lagret = Astro.cookies.get("tema")?.value
const tema = lagret === "light" || lagret === "dark" ? lagret : undefined
---
<html lang="nb" data-theme={tema}>

Hvitelist de to verdiene. En cookie kommer fra klienten, så den kan inneholde hva som helst: en gammel verdi, eller en noen har skrevet selv. Kopierer du den rett inn, får <html> en data-theme uten blokk, og siden faller til systemtemaet uten at noen av knappene er markert.

auto skal derfor gi ingen data-theme. Skriver du data-theme="auto", treffer ingen temablokk, og du har en verdi i markupen som ikke betyr noe.

Sett samtidig checked på den knappen som svarer til cookien, slik at gruppa viser det lagrede valget.

En helt statisk side har ingen server som kan lese cookien. Da er localStorage den eneste som varer, og skriptet må stå blokkerende i <head>, før siden tegnes:

<!-- I <head>, uten defer, før siden tegnes -->
<script>
const lagret = localStorage.getItem("tema")
if (lagret === "light" || lagret === "dark") {
document.documentElement.dataset.theme = lagret
}
</script>

Sjekk mot de to verdiene framfor mot «ikke auto». localStorage deles mellom alt på samme opphav, så nøkkelen kan inneholde noe annet enn det du skrev: en gammel verdi, eller en annen app sin. Skriver du den rett inn, får <html> en data-theme uten blokk, og siden faller til systemtemaet uten at noen av knappene er markert.

Står skriptet nederst i <body>, eller har det defer, rekker siden å tegne seg i feil tema først.

Noe må også lagre valget. Dette kan stå hvor som helst etter gruppa:

<script type="module">
const lagret = localStorage.getItem("tema")
const valgt = lagret === "light" || lagret === "dark" ? lagret : "auto"
for (const knapp of document.querySelectorAll(".fs-theme-control")) {
knapp.checked = knapp.value === valgt
knapp.addEventListener("change", () => {
localStorage.setItem("tema", knapp.value)
// Attributtet fra skriptet i <head> må bort, ellers står det foran
// «Følg systemet», som ikke har noen temablokk å vinne med.
delete document.documentElement.dataset.theme
})
}
</script>
  • Å lagre valget. Kontrollen bytter tema og husker ingenting. Varigheten er appens, gjennom en cookie eller localStorage.
  • Å virke inne i en skyggerot. :has() ser ikke ut av sitt eget tre, så en kontroll i en skyggerot setter ikke tema på siden. Den må stå i det samme treet som <html>.
  • Å avgjøre mellom to velgere. Står det to grupper på siden, altså to ulike name eller to skjemaer, er én knapp avkrysset i hver. Sier de ulike ting, kan ikke CSS vite hvilken som gjelder, og tokenene og color-scheme kan ende på hver sin. Bruk én gruppe per side.
  • Å gi en del av siden sitt eget tema. Kontrollen gjelder hele dokumentet. Skal bare et utsnitt ha sitt eget tema, er det data-theme på det utsnittet, og det er en temagrense.
  • Å trenge color-scheme fra deg. Hvert valg setter den selv, «Følg systemet» medregnet. Skriver du den på <html> utenfor et lag, slår regelen @layer fristil, og nettleserens egne flater blir låst til systemet mens brukeren har valgt noe annet.