All posts

Four themes, one stylesheet

The palette button in the navbar switches this site between four looks: Daylight, Midnight, Terminal and Void. None of them is a separate stylesheet. They are one set of semantic tokens on :root and three blocks that override them.

Semantic, not literal

The early version of the stylesheet used colour names as variables: --navy, --sky, --sun. That falls apart the moment a dark theme wants the "navy" to be white. The fix was naming tokens after their job:

:root {
  --bg: #f4f9fc;        /* page */
  --surface: #ffffff;   /* cards, menus */
  --ink: #12233d;       /* headings */
  --accent: #1b3358;    /* buttons, marks */
  --accent-ink: #ffffff;
  --hot: #f0a531;       /* the sun: small live details only */
}

[data-theme="void"] {
  --bg: #000000;
  --surface: #0b0b0b;
  --ink: #f7f7f7;
  --accent: #f7f7f7;
  --accent-ink: #000000;
}

Every component reads the semantic token, so the Void block is thirty lines and the whole site follows.

Applying it before the first paint

The chosen theme is stored in localStorage. A tiny inline script in the document head reads it and sets data-theme on <html> before the stylesheet applies, so there is no flash of the wrong theme. With nothing saved, prefers-color-scheme decides between Daylight and Midnight.

Terminal gets a little more

Terminal is the one theme that changes more than colours. It swaps the display face for the mono font, drops the italic, squares off the corners, and lays a faint scanline gradient over the page. All of that is still just CSS under [data-theme="terminal"].

Recolouring the 3D scene

The hero has a Three.js constellation behind the emblem. Its materials cannot read CSS variables directly, so the scene asks for them:

const cssColor = (token, fallback) => {
  const value = getComputedStyle(document.documentElement).getPropertyValue(token).trim();
  return new Color(value || fallback);
};

document.documentElement.addEventListener('themechange', updateColors);

The theme picker dispatches themechange after setting the attribute, and the scene copies the new colours into its materials. The constellation turns white in Void, green in Terminal, and sky blue everywhere else without a reload.