Skip to content
105 linesCodeBlameRaw
1---
2// The header's theme button: one icon, for Auto (the system's), Light or
3// Dark, and a click moves to the next. The same choice, kept in the same
4// place, as Starlight's theme select at the foot of the phone menu, so
5// the two stay in step.
6import { Monitor, Moon, Sun } from '@lucide/astro';
7---
8
9<button type="button" class="g1t-theme" data-g1t-theme aria-label="Theme: Auto">
10 <Monitor class="g1t-theme-auto" />
11 <Sun class="g1t-theme-light" />
12 <Moon class="g1t-theme-dark" />
13</button>
14
15<script is:inline>
16 // Before the first paint, so the button never shows the wrong icon.
17 (() => {
18 let stored = null;
19 try {
20 stored = localStorage.getItem('starlight-theme');
21 } catch {}
22 const choice = stored === 'light' || stored === 'dark' ? stored : 'auto';
23 for (const button of document.querySelectorAll('[data-g1t-theme]')) button.dataset.choice = choice;
24 })();
25</script>
26
27<script>
28 type Theme = 'auto' | 'light' | 'dark';
29 const NEXT: Record<Theme, Theme> = { auto: 'light', light: 'dark', dark: 'auto' };
30 const NAMES: Record<Theme, string> = { auto: 'Auto', light: 'Light', dark: 'Dark' };
31 const KEY = 'starlight-theme';
32
33 const load = (): Theme => {
34 try {
35 const stored = localStorage.getItem(KEY);
36 return stored === 'light' || stored === 'dark' ? stored : 'auto';
37 } catch {
38 return 'auto';
39 }
40 };
41
42 function show(theme: Theme) {
43 document.querySelectorAll<HTMLButtonElement>('[data-g1t-theme]').forEach((button) => {
44 button.dataset.choice = theme;
45 button.setAttribute('aria-label', `Theme: ${NAMES[theme]}`);
46 });
47 }
48
49 function choose(theme: Theme) {
50 const system = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
51 document.documentElement.dataset.theme = theme === 'auto' ? system : theme;
52 try {
53 localStorage.setItem(KEY, theme === 'auto' ? '' : theme);
54 } catch {}
55 // Starlight's own select, in the phone menu, shows the same choice.
56 (window as { StarlightThemeProvider?: { updatePickers(theme: Theme): void } }).StarlightThemeProvider?.updatePickers(
57 theme,
58 );
59 show(theme);
60 }
61
62 show(load());
63 document.querySelectorAll<HTMLButtonElement>('[data-g1t-theme]').forEach((button) => {
64 button.addEventListener('click', () => choose(NEXT[load()]));
65 });
66 // A choice made in the phone menu's select.
67 document.addEventListener('change', (event) => {
68 if ((event.target as Element).closest('starlight-theme-select')) show(load());
69 });
70</script>
71
72<style>
73 .g1t-theme {
74 display: inline-flex;
75 align-items: center;
76 justify-content: center;
77 width: 2rem;
78 height: 2rem;
79 border: 0;
80 border-radius: 0.5rem;
81 padding: 0;
82 background: transparent;
83 color: var(--g1t-muted);
84 cursor: pointer;
85 transition: color 0.15s, background 0.15s;
86 }
87 .g1t-theme:hover {
88 background: var(--g1t-raised);
89 color: var(--g1t-fg);
90 }
91 .g1t-theme:focus-visible {
92 outline: 2px solid var(--g1t-accent);
93 outline-offset: 1px;
94 }
95 .g1t-theme :global(svg) {
96 display: none;
97 width: 1rem;
98 height: 1rem;
99 }
100 .g1t-theme[data-choice='auto'] :global(.g1t-theme-auto),
101 .g1t-theme[data-choice='light'] :global(.g1t-theme-light),
102 .g1t-theme[data-choice='dark'] :global(.g1t-theme-dark) {
103 display: block;
104 }
105</style>