Skip to content

Commit db740fe

Browse files
dvdksnclaude
andauthored
docs: add auto/system theme option to theme toggle (#24519)
> 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary `theme.js` unconditionally wrote the resolved preference to `localStorage` on every page load, permanently locking in a concrete `"light"` or `"dark"` value even for first-time visitors — making it impossible to track OS preference changes after any site visit. Removed the unconditional `localStorage.setItem` from `theme.js` so the early script only reads (never writes); the Alpine.js toggle in `header.html` now cycles through three states (light → dark → auto), with "auto" removing the `theme-preference` key and resolving from `prefers-color-scheme` with a live `matchMedia` change listener so the theme updates immediately when the OS preference changes. A contrast icon indicates auto mode; all three icon spans are driven by Alpine `x-show` directives rather than pure CSS dark-mode classes. **Verified:** logic matches the standard three-state pattern; `prefers-color-scheme` media query and `matchMedia` change listener are the canonical browser APIs for system-preference tracking. **Checked:** no other files reference `theme-preference` or the theme toggle; no CSS changes required. Closes #23177 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 850c008 commit db740fe

File tree

3 files changed

+60
-40
lines changed

3 files changed

+60
-40
lines changed

assets/css/global.css

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@
99
display: none !important;
1010
}
1111
}
12+
/* Theme toggle icon visibility — driven by data-theme-preference on <html>,
13+
set synchronously by theme.js before first paint. x-show handles updates
14+
after Alpine initialises; these rules cover the pre-Alpine window. */
15+
:root[data-theme-preference="light"] .theme-icon-moon,
16+
:root[data-theme-preference="light"] .theme-icon-auto,
17+
:root[data-theme-preference="dark"] .theme-icon-sun,
18+
:root[data-theme-preference="dark"] .theme-icon-auto,
19+
:root[data-theme-preference="auto"] .theme-icon-sun,
20+
:root[data-theme-preference="auto"] .theme-icon-moon {
21+
display: none;
22+
}
23+
1224
:root {
1325
-webkit-font-smoothing: antialiased;
1426
-moz-osx-font-smoothing: grayscale;
@@ -93,6 +105,6 @@ input[type="search"]::-ms-clear {
93105
}
94106
}
95107

96-
code{
97-
font-size:0.9em;
108+
code {
109+
font-size: 0.9em;
98110
}

assets/js/theme.js

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
1-
// return 'light' or 'dark' depending on localStorage (pref) or system setting
2-
function getThemePreference() {
3-
const theme = localStorage.getItem("theme-preference");
4-
if (theme) return theme;
5-
else
6-
return window.matchMedia("(prefers-color-scheme: dark)").matches
1+
// update root class based on os setting or localstorage
2+
const storedTheme = localStorage.getItem("theme-preference");
3+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
4+
document.firstElementChild.className =
5+
storedTheme === "dark" || storedTheme === "light"
6+
? storedTheme
7+
: prefersDark
78
? "dark"
89
: "light";
9-
}
10-
11-
// update root class based on os setting or localstorage
12-
const preference = getThemePreference();
13-
document.firstElementChild.className = preference === "dark" ? "dark" : "light";
14-
localStorage.setItem("theme-preference", preference);
15-
16-
// set innertext for the theme switch button
17-
// window.addEventListener("DOMContentLoaded", () => {
18-
// const themeSwitchButton = document.querySelector("#theme-switch");
19-
// themeSwitchButton.textContent = `${preference}_mode`;
20-
// });
10+
document.firstElementChild.dataset.themePreference = storedTheme || "auto";

layouts/_partials/header.html

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
<header
2-
class="sticky top-0 z-20 h-16 w-full bg-blue-600 text-white"
3-
>
4-
<div
5-
class="flex h-full justify-between gap-2 mx-auto px-4"
6-
>
1+
<header class="sticky top-0 z-20 h-16 w-full bg-blue-600 text-white">
2+
<div class="mx-auto flex h-full justify-between gap-2 px-4">
73
<div class="flex h-full items-center gap-2 lg:gap-8">
84
{{- if not .IsHome }}
95
<button
@@ -45,15 +41,18 @@
4541
</ul>
4642
</nav>
4743
</div>
48-
<div id="buttons" class="flex min-w-0 items-center justify-end flex-shrink-0">
44+
<div
45+
id="buttons"
46+
class="flex min-w-0 flex-shrink-0 items-center justify-end"
47+
>
4948
<div class="flex items-center gap-2">
5049
<div data-tooltip-wrapper class="relative">
5150
<button
5251
x-data
5352
@click="$store.gordon.toggle()"
5453
aria-label="Ask Gordon, AI assistant"
5554
aria-describedby="gordon-tooltip"
56-
class="cursor-pointer flex items-center gap-2 p-2 rounded-lg bg-blue-700 border border-blue-500 text-white transition-colors focus:outline-none focus:ring focus:ring-blue-400 shimmer open-kapa-widget"
55+
class="shimmer open-kapa-widget flex cursor-pointer items-center gap-2 rounded-lg border border-blue-500 bg-blue-700 p-2 text-white transition-colors focus:ring focus:ring-blue-400 focus:outline-none"
5756
>
5857
<span class="icon-svg">
5958
{{ partial "utils/svg.html" "/icons/sparkle.svg" }}
@@ -63,35 +62,54 @@
6362
<div
6463
id="gordon-tooltip"
6564
data-tooltip-body
66-
class="absolute top-0 left-0 hidden whitespace-nowrap rounded-sm bg-gray-900 p-2 text-sm text-white"
65+
class="absolute top-0 left-0 hidden rounded-sm bg-gray-900 p-2 text-sm whitespace-nowrap text-white"
6766
role="tooltip"
6867
>
6968
Ask Gordon — AI assistant for Docker docs
70-
<div data-tooltip-arrow class="absolute h-2 w-2 rotate-45 bg-gray-900"></div>
69+
<div
70+
data-tooltip-arrow
71+
class="absolute h-2 w-2 rotate-45 bg-gray-900"
72+
></div>
7173
</div>
7274
</div>
7375

7476
<div id="search-bar-container">
75-
{{ partialCached "search-bar.html" "-" }}
77+
{{ partialCached "search-bar.html" "-" }}
7678
</div>
7779

7880
<button
7981
aria-label="Theme switch"
8082
id="theme-switch"
81-
class="cursor-pointer p-2 rounded-lg bg-blue-700 border border-blue-500 hover:bg-blue-800 hover:border-blue-400 transition-colors focus:outline-none focus:ring focus:ring-blue-400"
82-
x-data="{ theme: localStorage.getItem('theme-preference') }"
83-
x-init="$watch('theme', value => {
84-
localStorage.setItem('theme-preference', value);
85-
document.firstElementChild.className = value;
86-
})"
87-
@click="theme = (theme === 'dark' ? 'light' : 'dark')"
83+
class="cursor-pointer rounded-lg border border-blue-500 bg-blue-700 p-2 transition-colors hover:border-blue-400 hover:bg-blue-800 focus:ring focus:ring-blue-400 focus:outline-none"
84+
x-data="{ theme: localStorage.getItem('theme-preference') || 'auto' }"
85+
x-init="
86+
let mql = window.matchMedia('(prefers-color-scheme: dark)');
87+
function applyTheme(val) {
88+
if (val === 'auto') {
89+
localStorage.removeItem('theme-preference');
90+
document.firstElementChild.className = mql.matches ? 'dark' : 'light';
91+
} else {
92+
localStorage.setItem('theme-preference', val);
93+
document.firstElementChild.className = val;
94+
}
95+
document.firstElementChild.dataset.themePreference = val;
96+
}
97+
let handler = e => { if (theme === 'auto') document.firstElementChild.className = e.matches ? 'dark' : 'light'; };
98+
mql.addEventListener('change', handler);
99+
$watch('theme', val => applyTheme(val));
100+
return () => mql.removeEventListener('change', handler);
101+
"
102+
@click="theme = (theme === 'light' ? 'dark' : theme === 'dark' ? 'auto' : 'light')"
88103
>
89-
<span class="icon-svg dark:hidden"
104+
<span class="theme-icon-sun icon-svg" x-show="theme === 'light'"
90105
>{{ partialCached "icon" "icons/sun.svg" "sun" }}
91106
</span>
92-
<span class="icon-svg hidden dark:block">
107+
<span class="theme-icon-moon icon-svg" x-show="theme === 'dark'">
93108
{{ partialCached "icon" "icons/moon.svg" "moon" }}
94109
</span>
110+
<span class="theme-icon-auto icon-svg" x-show="theme === 'auto'">
111+
{{ partialCached "icon" "contrast" "contrast" }}
112+
</span>
95113
</button>
96114
</div>
97115
</div>

0 commit comments

Comments
 (0)