ThemeToggle.tsx
1 import { useEffect, useState } from 'react' 2 3 type ThemeMode = 'light' | 'dark' | 'auto' 4 5 function getInitialMode(): ThemeMode { 6 if (typeof window === 'undefined') { 7 return 'auto' 8 } 9 10 const stored = window.localStorage.getItem('theme') 11 if (stored === 'light' || stored === 'dark' || stored === 'auto') { 12 return stored 13 } 14 15 return 'auto' 16 } 17 18 function applyThemeMode(mode: ThemeMode) { 19 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches 20 const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode 21 22 document.documentElement.classList.remove('light', 'dark') 23 document.documentElement.classList.add(resolved) 24 25 if (mode === 'auto') { 26 document.documentElement.removeAttribute('data-theme') 27 } else { 28 document.documentElement.setAttribute('data-theme', mode) 29 } 30 31 document.documentElement.style.colorScheme = resolved 32 } 33 34 export default function ThemeToggle() { 35 const [mode, setMode] = useState<ThemeMode>('auto') 36 37 useEffect(() => { 38 const initialMode = getInitialMode() 39 setMode(initialMode) 40 applyThemeMode(initialMode) 41 }, []) 42 43 useEffect(() => { 44 if (mode !== 'auto') { 45 return 46 } 47 48 const media = window.matchMedia('(prefers-color-scheme: dark)') 49 const onChange = () => applyThemeMode('auto') 50 51 media.addEventListener('change', onChange) 52 return () => { 53 media.removeEventListener('change', onChange) 54 } 55 }, [mode]) 56 57 function toggleMode() { 58 const nextMode: ThemeMode = 59 mode === 'light' ? 'dark' : mode === 'dark' ? 'auto' : 'light' 60 setMode(nextMode) 61 applyThemeMode(nextMode) 62 window.localStorage.setItem('theme', nextMode) 63 } 64 65 const label = 66 mode === 'auto' 67 ? 'Theme mode: auto (system). Click to switch to light mode.' 68 : `Theme mode: ${mode}. Click to switch mode.` 69 70 return ( 71 <button 72 type="button" 73 onClick={toggleMode} 74 aria-label={label} 75 title={label} 76 className="rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm font-semibold text-[var(--sea-ink)] shadow-[0_8px_22px_rgba(30,90,72,0.08)] transition hover:-translate-y-0.5" 77 > 78 {mode === 'auto' ? 'Auto' : mode === 'dark' ? 'Dark' : 'Light'} 79 </button> 80 ) 81 }