Implementing Theme Provider for Dynamic Theme Switching
Theme is a set of design tokens: colors, typography, spacing, radius, shadows. Theme Provider is a mechanism that makes these tokens available to all components and allows switching them without page reload.
Two approaches: CSS Custom Properties (native CSS variables) or Context + CSS-in-JS. First is faster, simpler, framework-independent. Second provides more flexibility for dynamic tokens.
CSS Custom Properties approach
Theme lives in CSS, JavaScript only switches class or attribute on <html>:
/* themes.css */
:root,
[data-theme='light'] {
--color-bg: #ffffff;
--color-text: #0f172a;
--color-primary: #2563eb;
--radius-md: 8px;
--font-sans: 'Inter', system-ui, sans-serif;
}
[data-theme='dark'] {
--color-bg: #0f172a;
--color-text: #f1f5f9;
--color-primary: #3b82f6;
}
ThemeProvider
type ThemeId = 'light' | 'dark' | 'system'
interface ThemeContextValue {
theme: ThemeId
resolvedTheme: 'light' | 'dark'
setTheme: (theme: ThemeId) => void
themes: ThemeId[]
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
const STORAGE_KEY = 'app-theme'
const THEMES: ThemeId[] = ['system', 'light', 'dark']
function getSystemTheme(): 'light' | 'dark' {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<ThemeId>(() => {
if (typeof window === 'undefined') return 'system'
return (localStorage.getItem(STORAGE_KEY) as ThemeId) ?? 'system'
})
const resolvedTheme = useMemo<'light' | 'dark'>(() => {
if (theme === 'system') return getSystemTheme()
return theme as 'light' | 'dark'
}, [theme])
// Apply theme to <html>
useEffect(() => {
const root = document.documentElement
root.setAttribute('data-theme', resolvedTheme)
// Browser chrome color (Chrome Mobile)
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
const colors: Record<string, string> = {
light: '#ffffff',
dark: '#0f172a',
}
metaThemeColor?.setAttribute('content', colors[resolvedTheme])
}, [resolvedTheme])
const setTheme = (newTheme: ThemeId) => {
setThemeState(newTheme)
localStorage.setItem(STORAGE_KEY, newTheme)
}
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, themes: THEMES }}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme must be used within ThemeProvider')
return context
}
Usage in components
function ThemeSwitcher() {
const { theme, themes, setTheme } = useTheme()
return (
<select value={theme} onChange={(e) => setTheme(e.target.value as ThemeId)}>
{themes.map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
)
}







