Building a Seamless Light/Dark Theme Toggle with Local Storage
Learn how to build a smooth, CSS-variable-driven light and dark mode toggle that remembers user preferences using JavaScript and localStorage.
Modern web development demands responsive, user-friendly interfaces. Giving users the option to switch to a dark theme reduces eye strain and looks sleek. In this walkthrough, we will build a smooth, CSS-variable-driven theme toggle that remembers the user's preference across page loads using localStorage.
Interactive Demo
Flip the switch to toggle the theme. Refresh the page, and notice how it remembers your choice!
Implementation Details
To keep things modular and prevent messy overrides, this technique relies entirely on CSS Custom Properties (Variables). We define our colors at the top level, and when the user toggles the switch, we simply apply a .dark-mode class that redefines those variables. The browser handles the rest natively.
1. The HTML Structure
We use a standard hidden checkbox wrapped in a label. The slider span acts as the visual switch. We wrap our content inside a main container (theme-demo-wrapper) that will receive our dark mode class.
<div class="theme-demo-wrapper" id="liveDemoWrapper">
<div class="toggle-container">
<span>Light</span>
<label class="switch">
<input type="checkbox" id="demoToggleSwitch">
<span class="slider"></span>
</label>
<span>Dark</span>
</div>
<div class="demo-card">
<h4>Demo Card Component</h4>
</div>
</div>
2. The CSS (Variables & Transitions)
Here, we declare our base light colors. When the .dark-mode class is added via JavaScript, it replaces the variable values. Applying a transition to the background color ensures the theme fades in smoothly rather than flashing instantly.
/* Default Light Mode Variables */
.theme-demo-wrapper {
--demo-bg: #f8f9fa;
--demo-text: #212529;
--demo-card-bg: #ffffff;
background-color: var(--demo-bg);
color: var(--demo-text);
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Dark Mode Variable Overrides */
.theme-demo-wrapper.dark-mode {
--demo-bg: #1a1b1e;
--demo-text: #e9ecef;
--demo-card-bg: #2c2e33;
}
/* Switch styling & animation */
.switch { position: relative; display: inline-block; width: 46px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 24px; }
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }
/* Animate the slider when checked */
input:checked + .slider { background-color: #3b82f6; }
input:checked + .slider:before { transform: translateX(22px); }
3. The JavaScript & Local Storage
This is where the real magic happens. If a user chooses dark mode, they expect it to stay dark when they click to another page. We achieve this using the browser's localStorage API.
- On Load: We check
localStorage.getItem()to see if a theme was previously saved. If it was "dark", we apply the class immediately and set the checkbox to true. - On Click: We listen for the toggle switch. Depending on its state, we apply/remove the CSS class and use
localStorage.setItem()to save the choice for the next visit.
document.addEventListener('DOMContentLoaded', () => {
const toggleInput = document.getElementById('demoToggleSwitch');
const demoWrapper = document.getElementById('liveDemoWrapper');
if (!toggleInput || !demoWrapper) return;
// 1. Check for a previously saved theme preference
const currentTheme = localStorage.getItem('theme-preference');
if (currentTheme === 'dark') {
demoWrapper.classList.add('dark-mode');
toggleInput.checked = true; // Visually update the switch
}
// 2. Listen for changes and save to local storage
toggleInput.addEventListener('change', (e) => {
if (e.target.checked) {
demoWrapper.classList.add('dark-mode');
localStorage.setItem('theme-preference', 'dark');
} else {
demoWrapper.classList.remove('dark-mode');
localStorage.setItem('theme-preference', 'light');
}
});
});