|
1 | 1 | import { useState, useEffect, useCallback } from 'react'; |
2 | 2 |
|
| 3 | +/** @returns {'dark'|'light'} The current OS-level color scheme. */ |
| 4 | +const getSystemTheme = () => |
| 5 | + matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; |
| 6 | + |
3 | 7 | /** |
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. |
8 | 11 | */ |
9 | | -const applyTheme = theme => { |
| 12 | +const applyTheme = pref => { |
| 13 | + const theme = pref === 'system' ? getSystemTheme() : pref; |
10 | 14 | document.documentElement.setAttribute('data-theme', theme); |
11 | 15 | document.documentElement.style.colorScheme = theme; |
12 | | - localStorage.setItem('theme', theme); |
13 | 16 | }; |
14 | 17 |
|
15 | 18 | /** |
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]} |
17 | 28 | */ |
18 | 29 | 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 | + ); |
20 | 34 |
|
| 35 | + // Apply theme on every preference change, and if 'system', |
| 36 | + // also listen for OS-level color scheme changes. |
21 | 37 | 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]); |
33 | 48 |
|
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 | + } |
46 | 55 | }, []); |
47 | 56 |
|
48 | | - return [theme, toggleTheme]; |
| 57 | + return [pref, setTheme]; |
49 | 58 | }; |
0 commit comments