Skip to content

Commit 0306f18

Browse files
fix(theming): allow three states in useTheme (#656)
* refracto: theming * Update useTheme.mjs * Update useTheme.mjs Co-Authored-By: Aviv Keller <me@aviv.sh> * Update useTheme.mjs Co-Authored-By: Aviv Keller <me@aviv.sh> * Update useTheme.mjs --------- Co-authored-by: Aviv Keller <me@aviv.sh>
1 parent e45645a commit 0306f18

File tree

2 files changed

+44
-35
lines changed

2 files changed

+44
-35
lines changed

src/generators/web/ui/components/NavBar.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Logo from '#theme/Logo';
1313
* NavBar component that displays the headings, search, etc.
1414
*/
1515
export default () => {
16-
const [theme, toggleTheme] = useTheme();
16+
const [themePreference, setThemePreference] = useTheme();
1717

1818
return (
1919
<NavBar
@@ -23,8 +23,8 @@ export default () => {
2323
>
2424
<SearchBox />
2525
<ThemeToggle
26-
onClick={toggleTheme}
27-
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
26+
onChange={setThemePreference}
27+
currentTheme={themePreference}
2828
/>
2929
<a
3030
href={`https://github.com/${repository}`}
Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,58 @@
11
import { useState, useEffect, useCallback } from 'react';
22

3+
/** @returns {'dark'|'light'} The current OS-level color scheme. */
4+
const getSystemTheme = () =>
5+
matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
6+
37
/**
4-
* Applies the given theme to the `<html>` element's `data-theme` attribute
5-
* and persists the theme preference in `localStorage`.
6-
*
7-
* @param {string} theme - The theme to apply ('light' or 'dark').
8+
* Applies a theme to the document root.
9+
* Resolves 'system' to the actual OS preference before applying.
10+
* @param {'system'|'light'|'dark'} pref - The theme preference.
811
*/
9-
const applyTheme = theme => {
12+
const applyTheme = pref => {
13+
const theme = pref === 'system' ? getSystemTheme() : pref;
1014
document.documentElement.setAttribute('data-theme', theme);
1115
document.documentElement.style.colorScheme = theme;
12-
localStorage.setItem('theme', theme);
1316
};
1417

1518
/**
16-
* A React hook for managing the application's light/dark theme.
19+
* Applies the system theme to the document root.
20+
*/
21+
const applySystemTheme = () => applyTheme('system');
22+
23+
/**
24+
* React hook for managing theme preference.
25+
* Persists the choice to localStorage and listens for OS theme changes
26+
* when set to 'system'.
27+
* @returns {['system'|'light'|'dark', (next: 'system'|'light'|'dark') => void]}
1728
*/
1829
export const useTheme = () => {
19-
const [theme, setTheme] = useState('light');
30+
// Read stored preference once on mount; default to 'system'.
31+
const [pref, setPref] = useState(() =>
32+
SERVER ? 'system' : (localStorage.getItem('theme') ?? 'system')
33+
);
2034

35+
// Apply theme on every preference change, and if 'system',
36+
// also listen for OS-level color scheme changes.
2137
useEffect(() => {
22-
const initial =
23-
// Try to get the theme from localStorage first.
24-
localStorage.getItem('theme') ||
25-
// If not found, check the `data-theme` attribute on the document element
26-
document.documentElement.getAttribute('data-theme') ||
27-
// As a final fallback, check the user's system preference for dark mode.
28-
(matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
29-
30-
applyTheme(initial);
31-
setTheme(initial);
32-
}, []);
38+
applyTheme(pref);
39+
40+
if (pref !== 'system') {
41+
return;
42+
}
43+
44+
const mql = matchMedia('(prefers-color-scheme: dark)');
45+
mql.addEventListener('change', applySystemTheme);
46+
return () => mql.removeEventListener('change', applySystemTheme);
47+
}, [pref]);
3348

34-
/**
35-
* Callback function to toggle between 'light' and 'dark' themes.
36-
*/
37-
const toggleTheme = useCallback(() => {
38-
setTheme(prev => {
39-
// Determine the next theme based on the current theme.
40-
const next = prev === 'light' ? 'dark' : 'light';
41-
// Apply the new theme.
42-
applyTheme(next);
43-
// Return the new theme to update the state.
44-
return next;
45-
});
49+
/** Updates the preference in both React state and localStorage. */
50+
const setTheme = useCallback(next => {
51+
setPref(next);
52+
if (CLIENT) {
53+
localStorage.setItem('theme', next);
54+
}
4655
}, []);
4756

48-
return [theme, toggleTheme];
57+
return [pref, setTheme];
4958
};

0 commit comments

Comments
 (0)