Patterns Easy
Theme Toggle
Theme preference pattern for light, dark, and system mode with persistence and no-flash boot logic.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
* {
box-sizing: border-box;
}
:root {
--bg: #f1f5f9;
--panel: #ffffff;
--text: #0f172a;
--muted: #475569;
--border: rgba(15, 23, 42, 0.12);
--accent: #0284c7;
}
:root[data-theme="dark"] {
--bg: #020617;
--panel: #0f172a;
--text: #e2e8f0;
--muted: #94a3b8;
--border: rgba(255, 255, 255, 0.16);
--accent: #38bdf8;
}
body {
margin: 0;
font-family: "Sora", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
transition: background-color 0.2s ease, color 0.2s ease;
}
.shell {
width: min(720px, calc(100% - 2rem));
margin: 2rem auto;
}
p {
color: var(--muted);
}
.toggle {
display: inline-flex;
border: 1px solid var(--border);
border-radius: 999px;
overflow: hidden;
}
.toggle button {
border: 0;
background: transparent;
color: var(--muted);
padding: 0.45rem 0.8rem;
cursor: pointer;
}
.toggle button.active {
background: var(--accent);
color: #001018;
font-weight: 700;
}
.card {
margin-top: 1rem;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--panel);
padding: 1rem;
}
.card h2 {
margin-top: 0;
}
.card a {
color: var(--accent);
}(() => {
const key = "theme-mode";
const summary = document.getElementById("summary");
const buttons = Array.from(document.querySelectorAll("button[data-mode]"));
const media = window.matchMedia("(prefers-color-scheme: dark)");
const resolveMode = (mode) => (mode === "system" ? (media.matches ? "dark" : "light") : mode);
const apply = (mode) => {
const resolved = resolveMode(mode);
document.documentElement.setAttribute("data-mode", mode);
document.documentElement.setAttribute("data-theme", resolved);
localStorage.setItem(key, mode);
for (const button of buttons) {
button.classList.toggle("active", button.getAttribute("data-mode") === mode);
}
summary.textContent = `Mode: ${mode} (resolved to ${resolved})`;
};
for (const button of buttons) {
button.addEventListener("click", () => {
const mode = button.getAttribute("data-mode");
if (!mode) return;
apply(mode);
});
}
media.addEventListener("change", () => {
const currentMode = document.documentElement.getAttribute("data-mode") || "system";
if (currentMode === "system") apply("system");
});
apply(localStorage.getItem(key) || "system");
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Theme Toggle</title>
<script>
(function () {
var stored = localStorage.getItem("theme-mode");
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
var mode = stored || "system";
var resolved = mode === "system" ? (prefersDark ? "dark" : "light") : mode;
document.documentElement.setAttribute("data-theme", resolved);
document.documentElement.setAttribute("data-mode", mode);
})();
</script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<h1>Theme Toggle</h1>
<p id="summary"></p>
<div class="toggle" role="group" aria-label="Theme mode">
<button type="button" data-mode="light">Light</button>
<button type="button" data-mode="dark">Dark</button>
<button type="button" data-mode="system">System</button>
</div>
<article class="card">
<h2>Preview Card</h2>
<p>This pattern stores user preference and resolves against system mode.</p>
<a href="#">Action link</a>
</article>
</main>
<script src="script.js"></script>
</body>
</html>Theme Toggle
A robust theme integration pattern that handles user preference and system fallback safely.
Features
- Light, dark, and system options
- Preference persistence in localStorage
- No-flash startup theme application
- Accessible toggle controls