Реалізація Theme Provider для динамічної смени тем
Тема сайту — це набір дизайн-токенів: кольори, типографіка, відступи, радіусивай тіні. Theme Provider — механізм, який робить ці токени доступними для всіх компонентів та дозволяє переключатися між ними без перезавантаження сторінки.
Два підходи: CSS Custom Properties (нативні CSS-змінні) або Context + CSS-in-JS. Перший — швидший, простіший, незалежний від фреймворку. Другий — більше гнучкості для динамічних токенів.
Підхід на CSS Custom Properties
Тема живе в CSS, JavaScript тільки переключає клас або атрибут на <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])
// Застосовуємо тему до <html>
useEffect(() => {
const root = document.documentElement
root.setAttribute('data-theme', resolvedTheme)
// Колір строки браузера (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
}
Використання в компонентах
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>
)
}







