Dark Mode is a popular feature that lets users switch between light and dark themes. In this tutorial, we’ll walk through:

  1. Refactoring CSS to use variables for easy theming
  2. Adding a toggle button and wiring up the JavaScript
  3. 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.