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 localStorageLogic
- Check localStorage for a saved theme on page load
- Apply the saved theme or default to light
- Listen for click on the toggle button
- Toggle a
darkclass on the<html>element - Update the button icon to match the current theme
- Save the new preference to localStorage
- Use CSS variables on
:rootfor all colors — swapping theme changes everything at once
Flow
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:
| Step | Code | Result |
|---|---|---|
| Read storage | localStorage.getItem("theme") | "dark" |
| Apply theme | applyTheme("dark") | — |
| Add class | htmlElement.classList.add("dark") | <html class="dark"> |
| CSS kicks in | html.dark { --bg-primary: #1a1a2e; ... } | dark colors applied |
| Update icon | toggleIcon.textContent = "☀️" | button shows ☀️ |
| Update label | toggleLabel.textContent = "Light Mode" | button shows "Light Mode" |
Trace through clicking toggle when dark mode is active:
| Step | Code | Result |
|---|---|---|
| Check current | htmlElement.classList.contains("dark") | true |
| New theme | isDark ? "light" : "dark" | "light" |
| Apply | applyTheme("light") | removes "dark" class |
| CSS falls back | :root variables take effect | light colors applied |
| Save | localStorage.setItem("theme", "light") | saved |
| Update icon | "🌙" | button shows 🌙 |
Trace through first visit with nothing in localStorage:
| Step | Code | Result |
|---|---|---|
| Read storage | localStorage.getItem("theme") | null |
| Fallback | null || "light" | "light" |
| Apply | applyTheme("light") | light theme shown |
| No class | html 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