Santaji GadeJavaScript2 days ago5 Views

A dark mode toggle using JavaScript needs to check localStorage, then OS preference, then default — and apply it before the page even renders.
Table of Contents
ToggleA dark mode toggle using JavaScript needs to solve three things at once: read the user's saved preference, respect their OS setting when no preference exists yet, and apply the theme before the page actually renders to avoid a jarring flash of the wrong color scheme.
The pattern practitioners call the "preference cascade" checks three sources in order: localStorage first, since it reflects an explicit user choice, then the prefers-color-scheme media query as a fallback, then a default light theme if neither applies.
Getting the initial page load right matters more than the toggle button itself. Applying the theme class before the page renders prevents what's commonly called FOUC, a flash of unstyled or incorrectly themed content that briefly shows the wrong colors before JavaScript corrects it.
Callum's guide on DEV Community gets the ordering right: this inline script needs to sit at the very top of the head, executed as early as possible, before any CSS or content renders.
<script>
document.documentElement.classList.toggle(
'dark',
localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
);
</script>
Ollie Williams' guide highlights a simpler native option many implementations skip: setting a meta tag with name="color-scheme" and content="light dark" lets the browser handle native form controls, scrollbars, and other built-in UI elements automatically, without any custom CSS needed for those specific pieces.
Once the initial theme is set correctly, the toggle itself just needs to flip the class and save the new preference. whitep4nth3r's guide calls this the "preference cascade" in action, with the stored user preference always taking priority once it exists.
function getInitialTheme() {
const saved = localStorage.getItem('theme');
if (saved) return saved;
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
function applyTheme(theme) {
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
}
// Set initial theme on page load
applyTheme(getInitialTheme());
// Wire up the toggle button
document.getElementById('theme-toggle').addEventListener('click', () => {
const isDark = document.documentElement.classList.contains('dark');
applyTheme(isDark ? 'light' : 'dark');
});
The JavaScript only manages one class, all actual color values should live in CSS custom properties. This keeps the toggle logic simple and makes adding a third theme, or adjusting colors later, a CSS-only change.
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--border-color: #e0e0e0;
color-scheme: light dark;
}
html.dark {
--bg-color: #1a1a1a;
--text-color: #f0f0f0;
--border-color: #3a3a3a;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.2s ease, color 0.2s ease;
}
Some users switch their OS theme while your site is still open. Abbey Perini's guide covers listening for this properly, but only when the user hasn't explicitly overridden the setting themselves, otherwise a manual choice would get silently overwritten.
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
// Only respond to OS changes if the user hasn't set an explicit preference
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
💡 Quick Tip: test this behavior using Chrome DevTools' Rendering panel, which lets you emulate prefers-color-scheme without actually changing your OS settings, making it far faster to verify both branches of the logic.
A quick reference for the different ways to persist a theme preference.
| Storage Method | Persists Across | Best For |
|---|---|---|
| localStorage | Sessions, same browser only | Most sites, no backend required |
| Cookie + server-side | Devices, if tied to a user account | Logged-in apps wanting cross-device sync |
| prefers-color-scheme only | Nothing explicit, follows OS always | Simple sites with no manual override needed |
A short list to confirm before shipping a dark mode toggle to production.
Inline the initial theme script in the head, applying it after first paint causes a visible flash.
Check localStorage before OS preference, an explicit user choice should always win.
Keep all colors in CSS custom properties, never hardcode colors that need to change per theme.
Set color-scheme in CSS, this helps native browser UI like scrollbars and form controls match automatically.
Only listen for OS changes when no explicit choice exists, otherwise a manual toggle gets silently overridden.
This is the flash of wrong theme (FOUC) problem. It happens when the theme is applied after the page renders. Fix it by inlining a small script at the very top of the head that runs before first paint.
Check localStorage first. If the user has explicitly chosen a theme before, that choice should always take priority over their current OS-level setting.
CSS alone with prefers-color-scheme works if you only want to follow the OS setting. JavaScript is needed if you want to let users manually override that setting and remember their choice.
It tells the browser to automatically theme native UI elements, like scrollbars, form inputs, and date pickers, to match the current color scheme without needing custom CSS for each one.
Chrome DevTools has a Rendering panel that lets you emulate prefers-color-scheme directly in the browser, letting you test both light and dark branches without touching your actual system settings.
Check localStorage, then OS preference, then default
Inline the initial theme script to prevent a visible flash
Keep colors in CSS custom properties, not JavaScript
Set color-scheme so native browser UI matches automatically
Only auto-follow OS changes when no explicit choice exists
DevTools can emulate prefers-color-scheme for easier testing
Dark mode pairs well with other content UX features like scrollspy highlighting and auto-generated navigation. Explore both guides next.









