Dark/Light Modes
This docs site’s Theme control is Light and Dark only. The first
visit follows the OS (html.light or html.dark, localStorage.theme, and themeSource=system). A Light/Dark click is a
user choice (themeSource=user); later visits
remember that class even if the OS changes. After that click,
the same control becomes a menu of Light, Dark, and Reset to
system — the trigger still shows the current Light or Dark.
Reset follows the OS again and restores the simple toggle.
There is no persistent idle label named System.
Color, chart, and font tokens live on Theming.
FOUC script
Copying the two CSS files does not copy this docs site’s site.css or the inline script in src/index.html.
Paste this in <head> before the stylesheets so the
first paint already has html.light or html.dark.
If themeSource is system, or both theme and themeSource are missing,
the script re-reads prefers-color-scheme (the OS
may have changed overnight) and writes theme plus themeSource=system. If themeSource is user, or a legacy 24.3 theme exists
without a source, it applies the stored class and treats the
source as user. After this script, this site
always has a class. It also sets theme-color from the class (oklch(1 0 0) / oklch(0.145 0 0)).
<meta name="theme-color" content="oklch(1 0 0)">
<script>
try {
var theme = localStorage.getItem('theme');
var source = localStorage.getItem('themeSource');
var html = document.documentElement;
var os = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
if (source === 'system' || (!source && !theme)) {
theme = os;
source = 'system';
} else {
if (theme !== 'dark' && theme !== 'light')
theme = os;
source = 'user';
}
html.classList.add(theme);
localStorage.setItem('theme', theme);
localStorage.setItem('themeSource', source);
document.querySelector('meta[name="theme-color"]').content = theme === 'dark' ? 'oklch(0.145 0 0)' : 'oklch(1 0 0)';
} catch (e) {}
</script>Theme toggle
Optional. Same Light ↔ Dark toggle as this site’s control.
It updates html.dark / html.light, localStorage.theme, and themeSource, then theme-color from the class. While themeSource is system, a MediaQuery from svelte/reactivity applies OS changes with no
polling. After a user click, that control becomes a .menu dropdown: Light, Dark, and Reset to system.
The trigger still shows the current Light or Dark. Reset is
not a persistent idle label named System.
Reset writes themeSource=system, applies the
current OS, and restores the simple Light ↔ Dark toggle.
<script>
import { MediaQuery } from 'svelte/reactivity';
import { on_menu_toggle, menu_keydown, choose_item } from './ui/Place.svelte';
const THEME_LIGHT = 'oklch(1 0 0)';
const THEME_DARK = 'oklch(0.145 0 0)';
const THEME_LABEL = { light: 'Light', dark: 'Dark' };
const prefers_dark = new MediaQuery('prefers-color-scheme: dark');
function sync_theme_color() {
const meta = document.querySelector('meta[name="theme-color"]');
if (meta)
meta.setAttribute('content', document.documentElement.classList.contains('dark') ? THEME_DARK : THEME_LIGHT);
}
function os_mode() {
return prefers_dark.current ? 'dark' : 'light';
}
function read_source() {
try {
const stored_source = localStorage.getItem('themeSource');
const stored_theme = localStorage.getItem('theme');
if (stored_source === 'system' || (!stored_source && !stored_theme))
return 'system';
return 'user';
} catch {
return 'system';
}
}
function apply_theme(next, next_source) {
const html = document.documentElement;
html.classList.remove('dark', 'light');
html.classList.add(next);
try {
localStorage.setItem('theme', next);
localStorage.setItem('themeSource', next_source);
} catch {
/* ignore */
}
sync_theme_color();
}
function read_mode() {
if (typeof document === 'undefined')
return 'light';
const html = document.documentElement;
if (html.classList.contains('dark')) return 'dark';
if (html.classList.contains('light')) return 'light';
const next = os_mode();
apply_theme(next, source);
return next;
}
let source = $state(read_source());
let mode = $state(read_mode());
$effect(() => {
if (source !== 'system')
return;
const next = os_mode();
if (mode === next && document.documentElement.classList.contains(next))
return;
mode = next;
apply_theme(next, 'system');
});
function toggle_theme() {
mode = mode === 'dark' ? 'light' : 'dark';
source = 'user';
apply_theme(mode, 'user');
}
function choose_mode(event, next) {
mode = next;
source = 'user';
apply_theme(next, 'user');
choose_item(event);
}
function reset_to_system() {
source = 'system';
mode = os_mode();
apply_theme(mode, 'system');
}
function choose_reset(event) {
choose_item(event);
reset_to_system();
}
</script>
{#if source === 'system'}
<button type="button" onclick={toggle_theme} aria-label="Theme: {THEME_LABEL[mode]}">
{THEME_LABEL[mode]}
</button>
{:else}
<button type="button" popovertarget="theme-menu" aria-haspopup="menu" aria-expanded="false" aria-label="Theme: {THEME_LABEL[mode]}">
{THEME_LABEL[mode]}
</button>
<div id="theme-menu" popover class="menu" role="menu" tabindex="-1" data-side="bottom" data-align="end" ontoggle={on_menu_toggle} onkeydown={menu_keydown}>
<button class="menu-item" type="button" role="menuitemradio" aria-checked={mode === 'light'} onclick={(event) => choose_mode(event, 'light')}>
<span class="menu-check"></span>
Light
</button>
<button class="menu-item" type="button" role="menuitemradio" aria-checked={mode === 'dark'} onclick={(event) => choose_mode(event, 'dark')}>
<span class="menu-check"></span>
Dark
</button>
<button class="menu-item" type="button" role="menuitem" data-inset onclick={choose_reset}>
Reset to system
</button>
</div>
{/if}