Created
March 25, 2025 19:06
-
-
Save tyom/82e65963936bbe2bfe6591a64bc6cd94 to your computer and use it in GitHub Desktop.
ThemeProvider with hook to read and toggle theme (Vite)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
createContext, | |
useContext, | |
useEffect, | |
useSyncExternalStore, | |
useState, | |
} from 'react'; | |
type Theme = 'dark' | 'light' | 'system'; | |
type ThemeProviderProps = { | |
children: React.ReactNode; | |
defaultTheme?: Theme; | |
storageKey?: string; | |
}; | |
type ThemeProviderState = { | |
theme: Theme; | |
setTheme: (theme: Theme) => void; | |
}; | |
const initialState: ThemeProviderState = { | |
theme: 'system', | |
setTheme: () => null, | |
}; | |
const ThemeProviderContext = createContext<ThemeProviderState>(initialState); | |
export function ThemeProvider({ | |
children, | |
defaultTheme = 'system', | |
storageKey = 'vite-ui-theme', | |
...props | |
}: ThemeProviderProps) { | |
const [theme, setTheme] = useState<Theme>( | |
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme, | |
); | |
// Subscribe to system theme changes | |
const systemTheme = useSyncExternalStore( | |
(callback) => { | |
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); | |
mediaQuery.addEventListener('change', callback); | |
return () => mediaQuery.removeEventListener('change', callback); | |
}, | |
() => | |
window.matchMedia('(prefers-color-scheme: dark)').matches | |
? 'dark' | |
: 'light', | |
// Server-side snapshot (fallback to 'light' on server) | |
() => 'light', | |
); | |
// Apply theme to document | |
useEffect(() => { | |
const root = window.document.documentElement; | |
root.classList.remove('light', 'dark'); | |
root.classList.add(theme === 'system' ? systemTheme : theme); | |
}, [theme, systemTheme]); | |
const value = { | |
theme, | |
setTheme: (theme: Theme) => { | |
localStorage.setItem(storageKey, theme); | |
setTheme(theme); | |
}, | |
}; | |
return ( | |
<ThemeProviderContext.Provider {...props} value={value}> | |
{children} | |
</ThemeProviderContext.Provider> | |
); | |
} | |
export const useTheme = () => { | |
const context = useContext(ThemeProviderContext); | |
if (context === undefined) | |
throw new Error('useTheme must be used within a ThemeProvider'); | |
return context; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment