Why offer a dark mode?
Eye comfort
Dark UIs are easier on the eyes in low light and reduce glare at night.
Battery
On OLED screens, dark pixels use less power — real savings on phones.
Preference
Lots of people simply prefer it. Letting them choose feels considerate.
Respect the OS
Phones and laptops already have a system setting — a good site honors it.
Try the finished toggle
Here's exactly what we're building — a mini page with a theme button. Click the moon/sun to flip it. Every color comes from a CSS variable, so the whole UI changes at once.
Beautiful in any light.
One toggle, two themes, zero fuss.
This surface, the text, and the button all read from variables.
Put your colors in variables
The secret to dark mode: never hard-code colors. Define them once as CSS variables on :root (that's the
light theme), then redefine the same variables under a [data-theme="dark"] selector.
Your components read var(--…) and never need to know which theme is on.
:root {
--bg: #ffffff;
--text: #1d1d1f;
--surface: #f4f4f7;
--accent: #6366f1;
}
/* dark mode = the same names, different values */
:root[data-theme="dark"] {
--bg: #0f172a;
--text: #f1f5f9;
--surface: #1e293b;
--accent: #818cf8;
}
body { background: var(--bg); color: var(--text); }
.card { background: var(--surface); }
.btn { background: var(--accent); color: #fff; }
Add the toggle button
Switching themes is just adding or removing data-theme="dark" on the <html> element.
One button, one line of JavaScript.
<button id="themeBtn" aria-label="Toggle theme">🌙</button>
const root = document.documentElement; // the <html> tag
document.getElementById('themeBtn').addEventListener('click', () => {
const isDark = root.getAttribute('data-theme') === 'dark';
root.setAttribute('data-theme', isDark ? 'light' : 'dark');
});
Remember the choice
Right now the theme resets on every reload. Save it to localStorage when it changes, and read it back when
the page loads.
const root = document.documentElement;
// 1. on load, apply whatever was saved last time
const saved = localStorage.getItem('theme');
if (saved) root.setAttribute('data-theme', saved);
// 2. on click, flip it AND save it
document.getElementById('themeBtn').addEventListener('click', () => {
const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next); // remember it
});
Respect the system setting
If someone has never picked a theme, fall back to whatever their device is set to. The
prefers-color-scheme media query tells you, and you can read it from JavaScript with
matchMedia.
const root = document.documentElement;
const saved = localStorage.getItem('theme');
// saved choice wins; otherwise follow the operating system
const systemDark = matchMedia('(prefers-color-scheme: dark)').matches;
root.setAttribute('data-theme', saved || (systemDark ? 'dark' : 'light'));
You can also let pure CSS follow the system with no JavaScript at all — handy as a baseline:
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) { /* only if the user hasn't chosen */
--bg: #0f172a; --text: #f1f5f9;
}
}
The complete, copy-paste version
Drop this into any page. It applies the saved or system theme on load, toggles on click, remembers the choice, and swaps the button icon.
<!-- HTML --> <button id="themeBtn" aria-label="Toggle theme"> <span class="moon">🌙</span><span class="sun">☀️</span> </button>
/* CSS */
:root { --bg:#fff; --text:#1d1d1f; --accent:#6366f1; }
:root[data-theme="dark"] { --bg:#0f172a; --text:#f1f5f9; --accent:#818cf8; }
body { background: var(--bg); color: var(--text); transition: background .3s, color .3s; }
.sun { display: none; }
[data-theme="dark"] .moon { display: none; }
[data-theme="dark"] .sun { display: inline; }
// JS — run it as early as possible to avoid a flash
const root = document.documentElement;
const saved = localStorage.getItem('theme');
const systemDark = matchMedia('(prefers-color-scheme: dark)').matches;
root.setAttribute('data-theme', saved || (systemDark ? 'dark' : 'light'));
document.getElementById('themeBtn').addEventListener('click', () => {
const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
Make it feel right
- Stop the flash. Put the theme-applying line in a tiny
<script>in the<head>, before the page paints, so it never flashes the wrong theme on load. - Animate gently. A short
transitiononbackgroundandcolormakes the switch feel smooth, not jarring. - Label the button. Give it an
aria-label="Toggle theme"so screen readers announce it. - Swap the icon. Show a moon in light mode and a sun in dark mode so the button's purpose is obvious.
- Test both themes. Check color contrast in light and dark — a color that passes in one can fail in the other.