diff --git a/.changeset/bright-themes-guess.md b/.changeset/bright-themes-guess.md
new file mode 100644
index 00000000..6e65dc3e
--- /dev/null
+++ b/.changeset/bright-themes-guess.md
@@ -0,0 +1,5 @@
+---
+'@nodejs/doc-kit-generator-react': patch
+---
+
+Show the active light or dark theme icon when the system preference is selected.
diff --git a/packages/react/src/html/ui/components/ThemeToggle.jsx b/packages/react/src/html/ui/components/ThemeToggle.jsx
index 2e973755..e4e91e10 100644
--- a/packages/react/src/html/ui/components/ThemeToggle.jsx
+++ b/packages/react/src/html/ui/components/ThemeToggle.jsx
@@ -7,10 +7,10 @@ import withIsland from '../islands/withIsland.jsx';
* Theme switcher.
*/
const Toggle = () => {
- const [themePreference, setThemePreference] = useTheme();
+ const [currentTheme, setThemePreference] = useTheme();
return (
-
+
);
};
diff --git a/packages/react/src/html/ui/hooks/__tests__/theme.test.mjs b/packages/react/src/html/ui/hooks/__tests__/theme.test.mjs
new file mode 100644
index 00000000..2ffb22f3
--- /dev/null
+++ b/packages/react/src/html/ui/hooks/__tests__/theme.test.mjs
@@ -0,0 +1,16 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+
+import { getDisplayedTheme } from '../theme.mjs';
+
+describe('getDisplayedTheme', () => {
+ it('uses the resolved system scheme for the initial system preference', () => {
+ assert.strictEqual(getDisplayedTheme('system', true), 'dark');
+ assert.strictEqual(getDisplayedTheme('system', false), 'light');
+ });
+
+ it('preserves explicit user preferences', () => {
+ assert.strictEqual(getDisplayedTheme('dark', false), 'dark');
+ assert.strictEqual(getDisplayedTheme('light', true), 'light');
+ });
+});
diff --git a/packages/react/src/html/ui/hooks/theme.mjs b/packages/react/src/html/ui/hooks/theme.mjs
new file mode 100644
index 00000000..54958973
--- /dev/null
+++ b/packages/react/src/html/ui/hooks/theme.mjs
@@ -0,0 +1,17 @@
+/**
+ * Resolves the theme preference that should be represented by the toggle icon.
+ *
+ * The selector still stores `system` as the user preference, but its trigger
+ * should show the light or dark icon that is currently applied to the page.
+ *
+ * @param {'system'|'light'|'dark'} preference - The stored theme preference.
+ * @param {boolean} systemSupportsDarkMode - Whether the OS currently prefers dark mode.
+ * @returns {'light'|'dark'} The theme currently displayed by the page.
+ */
+export const getDisplayedTheme = (preference, systemSupportsDarkMode) => {
+ if (preference === 'system') {
+ return systemSupportsDarkMode ? 'dark' : 'light';
+ }
+
+ return preference;
+};
diff --git a/packages/react/src/html/ui/hooks/useTheme.mjs b/packages/react/src/html/ui/hooks/useTheme.mjs
index 3ed562d1..c1333096 100644
--- a/packages/react/src/html/ui/hooks/useTheme.mjs
+++ b/packages/react/src/html/ui/hooks/useTheme.mjs
@@ -1,5 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
+import { getDisplayedTheme } from './theme.mjs';
+
import { server } from '#theme/config';
/** @returns {'dark'|'light'} The current OS-level color scheme. */
@@ -33,6 +35,14 @@ export const useTheme = () => {
const [pref, setPref] = useState(() =>
server ? 'system' : (localStorage.getItem('theme') ?? 'system')
);
+ const [currentTheme, setCurrentTheme] = useState(() =>
+ server
+ ? 'system'
+ : getDisplayedTheme(
+ localStorage.getItem('theme') ?? 'system',
+ getSystemTheme() === 'dark'
+ )
+ );
// Apply theme on every preference change, and if 'system',
// also listen for OS-level color scheme changes.
@@ -44,8 +54,13 @@ export const useTheme = () => {
}
const mql = matchMedia('(prefers-color-scheme: dark)');
- mql.addEventListener('change', applySystemTheme);
- return () => mql.removeEventListener('change', applySystemTheme);
+ /** Synchronizes the icon when the OS-level color scheme changes. */
+ const handleSystemThemeChange = () => {
+ applySystemTheme();
+ setCurrentTheme(getSystemTheme());
+ };
+ mql.addEventListener('change', handleSystemThemeChange);
+ return () => mql.removeEventListener('change', handleSystemThemeChange);
}, [pref]);
/** Updates the preference in both React state and localStorage. */
@@ -53,8 +68,9 @@ export const useTheme = () => {
setPref(next);
if (!server) {
localStorage.setItem('theme', next);
+ setCurrentTheme(getDisplayedTheme(next, getSystemTheme() === 'dark'));
}
}, []);
- return [pref, setTheme];
+ return [currentTheme, setTheme];
};