JavaScript Tutorial
JavaScript Project: Theme Switch
Toggle light and dark on the page and remember the choice in localStorage.
What you will build
A page with a Light / Dark button. The colors swap immediately. Reload the preview and the last choice is still there, because it is stored in localStorage.
The theme is a class on document.documentElement (the html element). CSS custom properties change with that class. The script does not paint each heading by hand.
Open an example with Try it in JavaScript. That loads /javascript/try — a live page and a console.
Methods you will use
| Method | Job on this page |
|---|---|
classList | Toggles the dark class on <html> |
localStorage | Saves "light" or "dark" across reloads |
click | Runs the toggle when the button is pressed |
getItem / setItem | Reads the saved theme, then writes the new one |
textContent | Updates the button label to match the next action |
Apply the saved class before the first paint if you can. In this complete document the script sits at the end of the body, so you still set the class as the first line of the script to avoid a long flash.
Build in slices
Two color palettes on :root and on :root.dark. Body, card, and button all read the variables. No extra classes on those nodes.
Example
<style>
:root {
--bg: #fefce8;
--card: #fffbeb;
--ink: #1c1917;
--accent: #854d0e;
}
:root.dark {
--bg: #1c1917;
--card: #292524;
--ink: #fef3c7;
--accent: #fbbf24;
}
body { background: var(--bg); color: var(--ink); margin: 0; }
.card { background: var(--card); padding: 1rem; }
</style>
<article class="card">
<p>Light studio colors. Add class dark on html to swap the palette.</p>
</article>Restore first. If the stored value is "dark", add the class. Anything else, including a missing key, stays light.
Example
const root = document.documentElement;
const saved = localStorage.getItem("theme");
if (saved === "dark") root.classList.add("dark");Toggle writes both the class and the storage key. Use the return value of classList.toggle: it is true when the class is now present.
Example
<style>
:root { --bg: #fefce8; --ink: #1c1917; }
:root.dark { --bg: #1c1917; --ink: #fef3c7; }
body { background: var(--bg); color: var(--ink); }
</style>
<button id="theme" type="button">Use dark theme</button>
<script>
const root = document.documentElement;
const button = document.getElementById("theme");
const saved = localStorage.getItem("theme");
if (saved === "dark") root.classList.add("dark");
function syncLabel() {
const dark = root.classList.contains("dark");
button.textContent = dark ? "Use light theme" : "Use dark theme";
}
button.addEventListener("click", function () {
const dark = root.classList.toggle("dark");
localStorage.setItem("theme", dark ? "dark" : "light");
syncLabel();
});
syncLabel();
</script>Toggle once, reload the editor at /javascript/try. The dark palette should return. Try it in JavaScript stores the document; a full reload of that tab still readslocalStorage for the same origin.
Complete document
This file is the switch you would keep. Sample copy sits on the card so you can see headings, a paragraph, and a link in both palettes. The storage key is theme.
Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Theme Switch</title>
<style>
:root {
--bg: #fefce8;
--card: #fffbeb;
--ink: #1c1917;
--muted: #78716c;
--line: #fde68a;
--accent: #854d0e;
--accent-ink: #fffbeb;
}
:root.dark {
--bg: #1c1917;
--card: #292524;
--ink: #fef3c7;
--muted: #a8a29e;
--line: #44403c;
--accent: #fbbf24;
--accent-ink: #1c1917;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: var(--bg);
color: var(--ink);
font-family: "Segoe UI", system-ui, sans-serif;
}
.card {
width: min(24rem, 92vw);
background: var(--card);
border: 1px solid var(--line);
padding: 1.4rem 1.5rem 1.5rem;
}
h1 { margin: 0 0 0.4rem; font-size: 1.4rem; }
p { margin: 0 0 0.85rem; line-height: 1.55; color: var(--muted); }
a { color: var(--accent); }
button {
padding: 0.5rem 0.9rem;
border: 0;
background: var(--accent);
color: var(--accent-ink);
font: inherit;
font-weight: 700;
cursor: pointer;
}
</style>
</head>
<body>
<article class="card">
<h1>Theme Switch</h1>
<p>
Light is the yellow studio. Dark keeps the same layout with stone backgrounds and amber accents.
Reload the page after you toggle. The choice is stored in localStorage.
</p>
<p>
The live editor is <a href="/javascript/try">/javascript/try</a>, not /try.
</p>
<button id="theme" type="button">Use dark theme</button>
</article>
<script>
const root = document.documentElement;
const button = document.getElementById("theme");
const KEY = "theme";
function apply(theme) {
if (theme === "dark") root.classList.add("dark");
else root.classList.remove("dark");
button.textContent = theme === "dark" ? "Use light theme" : "Use dark theme";
}
const saved = localStorage.getItem(KEY);
apply(saved === "dark" ? "dark" : "light");
button.addEventListener("click", function () {
const next = root.classList.contains("dark") ? "light" : "dark";
localStorage.setItem(KEY, next);
apply(next);
});
</script>
</body>
</html>Storage is a string
localStorage only stores strings. Save "dark" and "light", not a boolean. getItem returns null when the key is missing. Treat that as light.
The key is shared with every page on the same origin. If another demo also uses "theme", they will overwrite each other. For a larger site you would namespace the key, for examplestudygrid-js-theme. This project keeps the short name.
Common mistakes
- Toggling a class on
bodywhile the CSS is written for:root.dark. Nothing changes. - Saving
truewithout quotes. You get the string"true". Compare it on purpose or save"dark". - Calling
classList.toggle("dark")and then alwayssetItem("theme", "dark"). Light would never persist. - Wrapping the first paint in
setTimeout. The light theme would flash every reload. - Using
sessionStorageby accident. A new tab would forget the choice. This project wantslocalStorage.
Practice tasks
- Honor
prefers-color-scheme: darkwhen nothing is stored yet. After the first click, storage wins. - Add a third High contrast theme and cycle Light → Dark → High contrast → Light.
- Show the stored value in a status line under the button. Preview at /javascript/try.