---
title: "Dark and light themes"
description: "A Firelight theme picker: System, Light and Dark, with colours from CSS custom properties and light-dark(), the choice kept in localStorage, and a head script so a stored theme doesn't flash."
---

# Dark and light themes

Build a theme picker with System, Light and Dark choices, that keeps the choice for the next visit.

- [MDN: light-dark()](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark)
- [MDN: color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme)

Pick Dark, then reload the page: the box stays dark. System follows your device's setting.

```fsharp
module Snippets.ThemePicker

open Fable.Core
open Browser
open Firelight
open type Firelight.Lit

let private key = "theme"

let private choices = [ "system", "System"; "light", "Light"; "dark", "Dark" ]

[<AttachMembers>]
type ThemePicker() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [ "theme", PropertyDeclaration<string>(state = true) ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.75rem; justify-items: start; }
        fieldset { display: flex; gap: 1rem; border: 1px solid var(--border); border-radius: 0.5rem; }
        /* The palette: each colour has a light and a dark value, picked by color-scheme. */
        .preview {
            --paper: light-dark(#fffdf9, #17151a);
            --ink: light-dark(#1f1d1a, #ece8e3);
            --line: light-dark(#e8e1d8, #3d3840);
            --accent: light-dark(#c2410c, #ff8a3d);
            color-scheme: light dark;
            background: var(--paper); color: var(--ink);
            border: 1px solid var(--line); border-radius: 0.5rem; padding: 0.75rem 1rem; width: 16rem;
        }
        .preview.light { color-scheme: light; }
        .preview.dark { color-scheme: dark; }
        .preview p { margin: 0 0 0.5rem; }
        .preview a { color: var(--accent); }
        """

    member val theme = "system" with get, set

    member this.Choose(theme: string) =
        this.theme <- theme

        try
            localStorage.setItem (key, theme)
        with _ ->
            ()

    member this.Restore() =
        try
            match localStorage.getItem key with
            | "light"
            | "dark" as theme -> this.theme <- theme
            | _ -> ()
        with _ ->
            ()

    // Once the first update is done, so the first render matches the prerendered HTML, and the
    // change starts an update of its own.
    override this.firstUpdated _ =
        promise {
            let! _ = this.updateComplete
            this.Restore()
        }
        |> Promise.start

    override this.render() =
        let choice (value: string, label: string) =
            html
                $"""
            <label>
                <input type="radio" name="theme" value={value} .checked={this.theme = value}
                    @change={fun _ -> this.Choose value}> {label}
            </label>"""

        html
            $"""
        <fieldset>
            <legend>Theme</legend>
            {choices |> List.map choice}
        </fieldset>
        <div class="preview {this.theme}">
            <p>This box follows the theme you pick. System follows your device's setting.</p>
            <a href="#how-it-works">How it works</a>
        </div>"""

defineElement<ThemePicker> "my-theme-picker"
```

```html
<my-theme-picker></my-theme-picker>
```

## How it works

The colours are CSS custom properties, each with a light and a dark value:
`--paper: light-dark(#fffdf9, #17151a)`. `light-dark()` picks one by the element's `color-scheme`.
`color-scheme: light dark` means "whichever the device prefers", and `light` or `dark` forces one.
So a theme is one property: the component's `render` adds the class `light` or `dark`, or neither,
and every colour follows. `color-scheme` also gives form controls and scroll bars the matching
look. The [Styling guide](https://roboz0r.github.io/Firelight/guides/styling/#theming-with-custom-properties) covers custom
properties across shadow DOM.

The choice is stored as a string. Anything other than `light` or `dark` reads as System, so a
stale or edited value can't break the page. [Remember state across visits](https://roboz0r.github.io/Firelight/cookbook/persist-state/)
covers storing more than a string.

## Theming a whole page

This demo themes a box, since the page around it has its own theme. For an app, put the palette on
`:root` in the page's stylesheet, and have the picker set `color-scheme` on the `<html>` element:

```fsharp
open Browser

let applyTheme (theme: string) =
    document.documentElement.style.setProperty ("color-scheme", if theme = "system" then "" else theme)

    try
        localStorage.setItem ("theme", theme)
    with _ ->
        ()
```

Custom properties inherit into every shadow root, so each component reads the palette with
`var(--paper)`.

A component's code runs after the page has painted, so a stored dark theme would first show light,
then switch. Set it before the first paint with a small script in the page's `<head>`, reading the
same key:

```html
<script>
  try {
    const theme = localStorage.getItem("theme");
    if (theme === "light" || theme === "dark") document.documentElement.style.colorScheme = theme;
  } catch {}
</script>
```

An inline script needs a hash or a nonce under a Content Security Policy that forbids inline
scripts. Prerendering can't help here: the build doesn't know which theme a visitor chose.

This site's theme button, in the header, works the same way, but in plain JavaScript in that head
script rather than as a component: a Firelight component in the header would load Lit on every
page, and pages without demos load no JavaScript but that script. It stores its choice under its
own key, so this demo's choice doesn't change the site's theme.

## Related

- [Styling](https://roboz0r.github.io/Firelight/guides/styling/), for custom properties, `:host` and parts.
- [Firelight.Context](https://roboz0r.github.io/Firelight/packages/context/), to pass a theme value to components that render
  differently by theme, not only colour.
