# Theme Switch

CSS has prefers-color-scheme media query. I made a CSS-based override using has() and checked pseudo-selectors, enhanced by Javascript to save preference.

# 1 HTML

The controls are in an HTML form:

<form id="theme-selector">
<input type="radio" id="light-theme" name="theme" value="light" /><label for="light-theme">light</label>
<input type="radio" id="default-theme" name="theme" value="default" checked /><label for="default-theme">default</label>
<input type="radio" id="dark-theme" name="theme" value="dark" /><label for="dark-theme">dark</label>
</form>

In my website I put it in the footer, because the header was pretty busy already.

# 2 CSS

The CSS has 3 main parts.

# 2.1 Colour Specification

Choose 6 colours as basis for themes.

:root
{
  --dark-foreground: #fff;
  --dark-background: #000;
  --dark-accent: #0f0;
  --light-foreground: #000;
  --light-background: #fff;
  --light-accent: #f00;
}

# 2.2 Adaptive Theme

This uses light theme if prefers-color-scheme is not set.

html
{
  --foreground: var(--light-foreground);
  --background: var(--light-background);
  --accent: var(--light-accent);
}

html:has(#dark-theme:checked)
{
  --foreground: var(--dark-foreground);
  --background: var(--dark-background);
  --accent: var(--dark-accent);
}

@media (prefers-color-scheme: dark)
{

html
{
  --foreground: var(--dark-foreground);
  --background: var(--dark-background);
  --accent: var(--dark-accent);
}

html:has(#light-theme:checked)
{
  --foreground: var(--light-foreground);
  --background: var(--light-background);
  --accent: var(--light-accent);
}

}

Force light theme for printing:

@media print
{

html,
html:has(#light-theme:checked),
html:has(#dark-theme:checked)
{
  --foreground: var(--light-foreground);
  --background: var(--light-background);
  --accent: var(--light-accent);
}

}

# 2.3 Page Styling

Set colours based on the 3 variables set by the adaptive theme.

*
{
  color: var(--foreground);
  background-color: var(--background);
}

a
{
  color: var(--accent);
  background-color: var(--background);
}

a:visited
{
  color: lch(from var(--accent) calc(l - 15) c calc(h - 45));
  background-color: var(--background);
}

# 3 Javascript

This is optional (can switch themes without it) but enhances experience by sharing preference across a multi-page site.

The Javascript keeps the selected theme synchronized to browser local storage.

const
main = _ => {
  const
  default_theme = document.querySelector("#default-theme"),
  dark_theme = document.querySelector("#dark-theme"),
  light_theme = document.querySelector("#light-theme"),
  set_theme = theme => {
    if (theme == "default") default_theme.checked = true;
    else if (theme == "dark") dark_theme.checked = true;
    else if (theme == "light") light_theme.checked = true;
  };
  set_theme(localStorage.getItem("theme"));
  window.addEventListener("storage", e => {
    if (e.key == "theme") set_theme(e.newValue);
  });
  for (theme of [default_theme, dark_theme, light_theme]) {
    theme.addEventListener("change", e => {
      localStorage.setItem("theme", e.target.value);
    });
  }
};

You need to add onload="main();" to the body tag in the HTML. TODO: figue out how to avoid this.

# 4 Server Side Cookies

Switching from local storage to cookies would allow server side scripting to check the desired option directly in the HTML, preventing momentary display of wrong theme during page load.

A button to submit theme preference to the server could work without Javascript, too.

# 5 Syntax Highlighting Filter

Suppose you have CSS for light theme code syntax highlighting:

code
{

/* Normal   */
color: #000;
background-color: white;

/* Function */ .fu,
/* Variable */ .va,
/* BuiltIn */ .bu
{
  color: #000;
  background-color: white;
}

/* Keyword  */ .kw,
/* ControlFlow */ .cf
{
  color: #000;
  background-color: white;
  font-weight: bold;
}

/* DataType */ .dt
{
  color: #080;
  background-color: white;
}

/* ... */
}

Then this can be adapted to dark theme with CSS filter:

html:has(#dark-theme:checked)
{

code,
code span
{
  filter: invert(1) hue-rotate(180deg);
}

}

@media (prefers-color-scheme: dark)
{

html:has(#default-theme:checked)
{

code,
code span
{
  filter: invert(1) hue-rotate(180deg);
}

}

}

Needs a bit of care not to apply the filter twice, which would undo the intention.