Implementing Dark Mode with CSS Variables and JavaScript
Dark Mode is a popular feature that lets users switch between light and dark themes. In this tutorial, we’ll walk through:
- Refactoring CSS to use variables for easy theming
- Adding a toggle button and wiring up the JavaScript
- Styling the toggle switch for a smooth slider effect
1. Refactor Your CSS to Use Variables
Expose your color palette as custom properties:
:root {
/* Light theme */
--bg-color: #ffffff;
--text-color: #333333;
--link-color: #1e88e5;
--border-color: #dddddd;
}
[data-theme="dark"] {
/* Dark theme overrides */
--bg-color: #121212;
--text-color: #e0e0e0;
--link-color: #64b5f6;
--border-color: #444444;
}
Then reference these variables in your styles:
body {
background-color: var(--bg-color);
color: var(--text-color);
}
a {
color: var(--link-color);
}
/* And so on for other selectors… */
Toggle the data-theme
attribute on <html>
and everything updates automatically.
2. Add the Toggle Button & JavaScript
Place this markup in your header:
<label class="switch">
<input type="checkbox" id="theme-toggle">
<span class="slider"></span>
</label>
Then, just before </body>
, add this script to initialize and persist the theme:
<script>
const toggleCheckbox = document.getElementById('theme-toggle');
let stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let theme = stored || (prefersDark ? 'dark' : 'light');
function applyTheme(t) {
if (t === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
toggleCheckbox.checked = true;
} else {
document.documentElement.removeAttribute('data-theme');
toggleCheckbox.checked = false;
}
localStorage.setItem("theme", t);
}
document.addEventListener("DOMContentLoaded", () => {
applyTheme(theme);
});
toggleCheckbox.addEventListener('change', () => {
theme = toggleCheckbox.checked ? 'dark' : 'light';
applyTheme(theme);
});
</script>
3. Style the Toggle Switch
Add minimal CSS to create the slider effect:
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--border-color);
border-radius: 28px;
transition: background-color 0.2s;
}
.slider::before {
content: "";
position: absolute;
height: 22px;
width: 22px;
left: 3px;
bottom: 3px;
background-color: var(--bg-color);
border-radius: 50%;
transition: transform 0.2s;
}
.switch input:checked + .slider {
background-color: var(--link-color);
}
.switch input:checked + .slider::before {
transform: translateX(22px);
}
And that’s it! You now have a fully functional Dark Mode switch that persists user preference and degrades gracefully when JavaScript is disabled.