Introduction
Dark mode has become a must-have feature for modern websites and applications, offering users a comfortable, low-light viewing experience that reduces eye strain and conserves battery life on OLED screens. Implementing dark mode using CSS variables and a simple JavaScript toggle is both elegant and maintainable: by defining your colors as variables, you can switch themes by updating just a few values. In this guide, you’ll learn how to structure your CSS variables, apply them to your UI components, and write a lightweight JavaScript toggle—plus, persist the user’s preference across sessions with localStorage
. Let’s dive in!

1. Setting Up Your CSS Variables
CSS variables (custom properties) let you define reusable values that can be dynamically overridden.
cssCopyEdit:root {
/* Light theme colors */
--bg-color: #ffffff;
--text-color: #333333;
--link-color: #0066cc;
--card-bg: #f9f9f9;
--border-color: #dddddd;
}
[data-theme="dark"] {
/* Dark theme overrides */
--bg-color: #121212;
--text-color: #eeeeee;
--link-color: #66aaff;
--card-bg: #1e1e1e;
--border-color: #333333;
}
/* Apply variables throughout your stylesheet */
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
a {
color: var(--link-color);
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 1rem;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
Why This Works
:root
defines default (light) theme variables.[data-theme="dark"]
overrides just those variables you want to change for dark mode.- UI components reference variables via
var(--variable-name)
, so switching themes is instantaneous.
2. Creating the Toggle Button
Add a simple HTML button to your layout—ideally in a consistent location, like the header:
htmlCopyEdit<header>
<button id="theme-toggle" aria-label="Toggle dark mode">🌓</button>
</header>
- The emoji provides a quick visual cue; you can swap in an icon or text as desired.
aria-label
ensures accessibility for screen-reader users.
3. Writing the JavaScript Toggle Logic
Use JavaScript to switch the data-theme
attribute on the <html>
(or <body>
) element and store the user’s choice in localStorage
.

javascriptCopyEdit(() => {
const toggle = document.getElementById('theme-toggle');
const root = document.documentElement;
const storedTheme = localStorage.getItem('theme');
// Initialize theme on page load
if (storedTheme) {
root.setAttribute('data-theme', storedTheme);
}
// Determine default (system) preference if no choice stored
else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.setAttribute('data-theme', 'dark');
}
// Toggle handler
toggle.addEventListener('click', () => {
const current = root.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
})();
How It Works
- Load Preference: On page load, check
localStorage
; if no stored theme, fall back to the user’s OS-level preference viaprefers-color-scheme
. - Toggle: Flip between
"dark"
and"light"
, update thedata-theme
attribute, and save the new choice. - Persistent: Because you save to
localStorage
, the site remembers the user’s selection on subsequent visits.
4. Enhancements and Best Practices

- Smooth Transition: Add
transition
rules (as shown) so colors fade rather than jump. - Accessibility: Ensure contrast ratios meet WCAG guidelines in both themes.
- Initial Flash Prevention: To avoid a flash of the wrong theme on load, you can inline a small script in your
<head>
that readslocalStorage
and sets the attribute before CSS loads: htmlCopyEdit<script> (function() { const theme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', theme); })(); </script>
- Icon Swap: Swap the toggle’s icon or text based on the active theme for user clarity: javascriptCopyEdit
function updateToggleIcon(theme) { toggle.textContent = theme === 'dark' ? '☀️' : '🌙'; } // Call updateToggleIcon when initializing and toggling
- Modular CSS: If using a CSS preprocessor or component-based framework, you can import your variables centrally and reference them in component styles.
Conclusion
By leveraging CSS variables for theming and a few lines of JavaScript for toggling and persistence, you can deliver a seamless dark mode experience with minimal overhead. This approach scales elegantly as your design system grows—just update variable definitions, and the entire UI responds. Try integrating this pattern into your next project, ensuring you test for contrast, performance, and accessibility to delight users who prefer or require dark mode.