DocsHub
JavascriptAdvanced

Theme Switcher

Build a theme switcher that toggles between light and dark mode and remembers the user's preference.

Theme Switcher

Problem

Build a theme switcher that toggles between light and dark mode. The chosen theme is saved to localStorage so when the user refreshes the page or comes back later their preference is remembered.

Page loads (first time)
→ Light theme by default

User clicks toggle
→ Switches to dark mode
→ Button icon changes to ☀️
→ Preference saved to localStorage

User refreshes page
→ Dark mode is restored automatically

User clicks toggle again
→ Switches back to light mode
→ Button icon changes to 🌙
→ Preference updated in localStorage

Logic

  1. Check localStorage for a saved theme on page load
  2. Apply the saved theme or default to light
  3. Listen for click on the toggle button
  4. Toggle a dark class on the <html> element
  5. Update the button icon to match the current theme
  6. Save the new preference to localStorage
  7. Use CSS variables on :root for all colors — swapping theme changes everything at once

Flow

yes no Page loads Saved themein localStorage? Apply saved theme Apply light theme default Update button icon User clicks toggle Toggle dark class on html Update button icon Save to localStorage

HTML Structure

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Theme Switcher</title>
    <style>
      /* Step 1 — define CSS variables on :root for light theme */
      :root {
        --bg-primary: #ffffff;
        --bg-secondary: #f5f5f5;
        --text-primary: #111111;
        --text-secondary: #555555;
        --border-color: #e0e0e0;
        --card-bg: #ffffff;
        --toggle-bg: #e0e0e0;
      }

      /* Step 2 — override variables for dark theme */
      /* applied when html element has the "dark" class */
      html.dark {
        --bg-primary: #1a1a2e;
        --bg-secondary: #16213e;
        --text-primary: #e0e0e0;
        --text-secondary: #a0a0a0;
        --border-color: #2a2a4a;
        --card-bg: #0f3460;
        --toggle-bg: #444;
      }

      /* Step 3 — all elements use CSS variables */
      /* switching the class on html changes everything instantly */
      * {
        box-sizing: border-box;
        transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
      }

      body {
        font-family: sans-serif;
        background-color: var(--bg-primary);
        color: var(--text-primary);
        min-height: 100vh;
        margin: 0;
        padding: 40px 20px;
      }

      /* top navbar */
      .navbar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        max-width: 600px;
        margin: 0 auto 40px;
      }

      .navbar h1 {
        font-size: 1.4rem;
        color: var(--text-primary);
        margin: 0;
      }

      /* toggle button */
      #theme-toggle {
        background: var(--toggle-bg);
        border: none;
        border-radius: 50px;
        padding: 8px 16px;
        font-size: 1.2rem;
        cursor: pointer;
        display: flex;
        align-items: center;
        gap: 8px;
        color: var(--text-primary);
        transition: background 0.3s;
      }

      #theme-toggle:hover {
        opacity: 0.85;
      }

      .toggle-label {
        font-size: 0.85rem;
        font-weight: 500;
      }

      /* content area */
      .content {
        max-width: 600px;
        margin: 0 auto;
      }

      /* example cards to show the theme in action */
      .card {
        background: var(--card-bg);
        border: 1px solid var(--border-color);
        border-radius: 10px;
        padding: 20px;
        margin-bottom: 16px;
      }

      .card h2 {
        margin: 0 0 8px;
        color: var(--text-primary);
        font-size: 1.1rem;
      }

      .card p {
        margin: 0;
        color: var(--text-secondary);
        font-size: 0.95rem;
        line-height: 1.6;
      }

      /* stat row inside a card */
      .stat-row {
        display: flex;
        gap: 16px;
        margin-top: 12px;
      }

      .stat {
        background: var(--bg-secondary);
        border-radius: 8px;
        padding: 10px 16px;
        flex: 1;
        text-align: center;
      }

      .stat .value {
        font-size: 1.4rem;
        font-weight: 700;
        color: var(--text-primary);
      }

      .stat .label {
        font-size: 0.8rem;
        color: var(--text-secondary);
      }
    </style>
  </head>
  <body>
    <!-- navbar with toggle button -->
    <div class="navbar">
      <h1>DocsHub</h1>
      <button id="theme-toggle">
        <span id="toggle-icon">🌙</span>
        <span class="toggle-label" id="toggle-label">Dark Mode</span>
      </button>
    </div>

    <!-- demo content so theme differences are visible -->
    <div class="content">
      <div class="card">
        <h2>Welcome to DocsHub</h2>
        <p>
          Your programming learning hub. Switch between light and dark mode
          using the button in the top right. Your preference is saved
          automatically.
        </p>
      </div>

      <div class="card">
        <h2>Your Progress</h2>
        <p>Keep track of your learning across all languages.</p>
        <div class="stat-row">
          <div class="stat">
            <div class="value">12</div>
            <div class="label">Topics Read</div>
          </div>
          <div class="stat">
            <div class="value">5</div>
            <div class="label">Exercises Done</div>
          </div>
          <div class="stat">
            <div class="value">3</div>
            <div class="label">Days Streak</div>
          </div>
        </div>
      </div>

      <div class="card">
        <h2>Currently Learning</h2>
        <p>
          JavaScript — Advanced Section. Next up: Memory Management.
        </p>
      </div>
    </div>

    <script src="script.js"></script>
  </body>
</html>

Solution

// Step 1 — select elements
const htmlElement = document.documentElement; // the <html> element
const themeToggle = document.querySelector("#theme-toggle");
const toggleIcon = document.querySelector("#toggle-icon");
const toggleLabel = document.querySelector("#toggle-label");

// Step 2 — load saved theme from localStorage on page load
// if nothing is saved — default to "light"
const savedTheme = localStorage.getItem("theme") || "light";

// Step 3 — apply the saved theme immediately
applyTheme(savedTheme);

// Step 4 — listen for toggle button click
themeToggle.addEventListener("click", () => {
  // check which theme is currently active
  const isDark = htmlElement.classList.contains("dark");

  // toggle to the opposite theme
  const newTheme = isDark ? "light" : "dark";

  // apply and save
  applyTheme(newTheme);
  localStorage.setItem("theme", newTheme);
});

// Step 5 — apply a theme by name
function applyTheme(theme) {
  if (theme === "dark") {
    // add "dark" class to <html>
    // CSS variables defined in html.dark override the :root defaults
    htmlElement.classList.add("dark");

    // update button to show "switch to light" option
    toggleIcon.textContent = "☀️";
    toggleLabel.textContent = "Light Mode";

  } else {
    // remove "dark" class — CSS variables fall back to :root defaults
    htmlElement.classList.remove("dark");

    // update button to show "switch to dark" option
    toggleIcon.textContent = "🌙";
    toggleLabel.textContent = "Dark Mode";
  }
}

Code Execution

Trace through page load with "dark" saved in localStorage:

StepCodeResult
Read storagelocalStorage.getItem("theme")"dark"
Apply themeapplyTheme("dark")
Add classhtmlElement.classList.add("dark")<html class="dark">
CSS kicks inhtml.dark { --bg-primary: #1a1a2e; ... }dark colors applied
Update icontoggleIcon.textContent = "☀️"button shows ☀️
Update labeltoggleLabel.textContent = "Light Mode"button shows "Light Mode"

Trace through clicking toggle when dark mode is active:

StepCodeResult
Check currenthtmlElement.classList.contains("dark")true
New themeisDark ? "light" : "dark""light"
ApplyapplyTheme("light")removes "dark" class
CSS falls back:root variables take effectlight colors applied
SavelocalStorage.setItem("theme", "light")saved
Update icon"🌙"button shows 🌙

Trace through first visit with nothing in localStorage:

StepCodeResult
Read storagelocalStorage.getItem("theme")null
Fallbacknull || "light""light"
ApplyapplyTheme("light")light theme shown
No classhtml has no "dark" class:root defaults active

Output

First visit             → Light theme, button shows 🌙 Dark Mode
Click toggle            → Dark theme, button shows ☀️ Light Mode, saved to localStorage
Refresh page            → Dark theme restored automatically
Click toggle again      → Light theme, button shows 🌙 Dark Mode, localStorage updated

On this page