Github

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}