feat: proper dark-mode support via color-scheme meta + light-dark() #1

Merged
lytedev merged 2 commits from dark-mode into master 2026-07-07 11:13:41 -05:00
Owner

Adds proper dark-mode support following https://olliewilliams.xyz/blog/dark-mode/.

What changed

  • color-scheme meta tag as source of truth<meta name="color-scheme" content="light dark"> in <head>; the OS preference is respected before CSS loads, and form controls / scrollbars / system colors now follow the theme.
  • light-dark() everywhere — every themed color is declared once in :root, replacing the duplicated light/dark variable blocks, body theme classes, and prefers-color-scheme media queries.
  • No flash of wrong theme — an inline script early in <head> restores the saved choice (kept the existing theme localStorage key, so existing visitors' saved preferences survive) and sets the mode class on <html> pre-paint, so the toggler icon doesn't flash either.
  • Toggle rewrites the meta taglight / dark, or light dark + cleared storage for system; same 3-way cycle as before.
  • .hide-in-{light,dark}-theme now key off the resolved scheme via a registered --used-scheme custom property + container style queries (prefers-color-scheme only reflects the OS setting, ignoring the toggle).
  • Print always uses the light scheme.

Verification

Headless Chromium (Playwright), 26 assertions, all passing:

  • system mode follows OS preference in both directions
  • 3-way cycle (system → non-OS theme → OS theme → system) updates colors, meta, storage, and icon
  • choice persists across reloads; meta is rewritten pre-load (no flash)
  • light-dark() resolves on arbitrary elements (drives form controls)
  • container-style-query helper classes reflect the resolved scheme
  • legacy stored "system" value from the old implementation still parses
  • screenshotted light + dark on a real post

Note

light-dark() requires a 2024+ browser (Chrome 123, Firefox 120, Safari 17.5). Older browsers get UA default colors. static/styles.css is untracked — rebuild via make build before make publish.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TKabGxxNaNoocaxN8B4BFG

Adds proper dark-mode support following https://olliewilliams.xyz/blog/dark-mode/. ## What changed - **`color-scheme` meta tag as source of truth** — `<meta name="color-scheme" content="light dark">` in `<head>`; the OS preference is respected before CSS loads, and form controls / scrollbars / system colors now follow the theme. - **`light-dark()` everywhere** — every themed color is declared once in `:root`, replacing the duplicated light/dark variable blocks, `body` theme classes, and `prefers-color-scheme` media queries. - **No flash of wrong theme** — an inline script early in `<head>` restores the saved choice (kept the existing `theme` localStorage key, so existing visitors' saved preferences survive) and sets the mode class on `<html>` pre-paint, so the toggler icon doesn't flash either. - **Toggle rewrites the meta tag** — `light` / `dark`, or `light dark` + cleared storage for system; same 3-way cycle as before. - **`.hide-in-{light,dark}-theme`** now key off the *resolved* scheme via a registered `--used-scheme` custom property + container style queries (`prefers-color-scheme` only reflects the OS setting, ignoring the toggle). - **Print** always uses the light scheme. ## Verification Headless Chromium (Playwright), 26 assertions, all passing: - system mode follows OS preference in both directions - 3-way cycle (system → non-OS theme → OS theme → system) updates colors, meta, storage, and icon - choice persists across reloads; meta is rewritten pre-load (no flash) - `light-dark()` resolves on arbitrary elements (drives form controls) - container-style-query helper classes reflect the resolved scheme - legacy stored `"system"` value from the old implementation still parses - screenshotted light + dark on a real post ## Note `light-dark()` requires a 2024+ browser (Chrome 123, Firefox 120, Safari 17.5). Older browsers get UA default colors. `static/styles.css` is untracked — rebuild via `make build` before `make publish`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TKabGxxNaNoocaxN8B4BFG
Converge theming onto the approach from
https://olliewilliams.xyz/blog/dark-mode/:

- a color-scheme meta tag ('light dark') is the single source of truth
  for the used scheme; it also makes form controls, scrollbars, and
  system colors follow the theme
- every themed color is declared once in :root via light-dark()
  instead of duplicated light/dark variable blocks + body classes +
  prefers-color-scheme media queries
- an inline script early in <head> restores the saved choice (existing
  'theme' localStorage key, so saved preferences survive) before any
  rendering, eliminating the flash of the wrong theme
- the toggle now rewrites the meta tag ('light'/'dark', or 'light dark'
  + cleared storage for system) and keeps its mode class on <html> so
  the icon can also render pre-paint without flashing
- .hide-in-{light,dark}-theme now key off the *resolved* scheme via a
  registered --used-scheme custom property + container style queries
  (prefers-color-scheme only reflects the OS, ignoring the toggle)
- print always uses the light scheme (color-scheme: light)

Verified with headless Chromium: both schemes render, the 3-way cycle
(system/dark/light) works, the choice persists across reloads, the meta
is rewritten pre-load, and legacy stored 'system' values still parse.

Note: light-dark() needs a 2024+ browser (Chrome 123, Firefox 120,
Safari 17.5); older browsers fall back to UA default colors.
'dark light' vs 'light dark' only matters as a fallback: per
https://www.w3.org/TR/css-color-adjust-1/#color-scheme-prop the user's
reported preference always wins when listed, and otherwise the first
listed scheme is used. The old CSS defaulted to dark unless the OS
explicitly preferred light, so dark-first preserves that intent.
lytedev merged commit c24afbe9b7 into master 2026-07-07 11:13:41 -05:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lytedev/site.lyte.dev!1
No description provided.