Vanilla
Framework-free theming
No React, no Vue, no Svelte — just a ThemeKit class. Drop it into any HTML page or plain-JS project and get mode switching, family selection, CSS variable binding, and localStorage persistence out of the box.
1Why Vanilla?
When your page is a plain HTML file, a server-rendered template, or a framework that Theme Kit doesn't ship an adapter for.
- You have a static HTML page, WordPress theme, or PHP template that doesn't use a JavaScript framework.
- You want the lightest possible integration — one import, one class, no build step required.
- You're embedding theme controls inside an admin panel, docs site, or embedded widget.
- The runtime is identical to what the React/Vue/Svelte adapters use internally — same persistence, same token resolution, same family logic.
2The ThemeKit class
Import the class, pass your options, and the instance handles everything — DOM binding, persistence, and event emission.
1import { ThemeKit } from "@theme-kit/core/vanilla";
2
3const kit = new ThemeKit({
4 defaultTheme: "light",
5});
6
7// Or use the static shorthand
8const kit = ThemeKit.init({ defaultTheme: "light" });Static shorthand |
ThemeKit.init(options) is identical to new ThemeKit(options) — pick whichever reads better.3Reading the active theme
Synchronous getters expose the active theme, its mode, and its family — plus a reactive event system.
1// The full active theme definition (name, tokens, mode, etc.)
2const current = kit.theme;
3
4// The current mode: "light", "dark", or "system"
5const mode = kit.mode;
6
7// The current family: "default", "plum", "mint", …
8const family = kit.family;
9
10// All registered themes
11const all = kit.themes;1// Fires when any theme change occurs
2const unsub = kit.on("themeChange", (theme) => {
3 console.log("New theme:", theme.name);
4});
5
6// Fires when only the mode changes
7kit.on("modeChange", (mode) => {
8 console.log("Mode:", mode);
9});
10
11// Fires when only the family changes
12kit.on("familyChange", (family) => {
13 console.log("Family:", family);
14});
15
16// Later — clean up
17unsub();4Switching themes
Switch mode, switch family, or toggle — every change is persisted and emits the appropriate event.
1// Switch mode — persisted to localStorage automatically
2kit.setMode("dark");
3kit.setMode("light");
4kit.setMode("system");
5
6// Switch family
7kit.setFamily("plum");
8kit.setFamily("mint");
9
10// Toggle between light and dark (ignores "system")
11kit.toggleTheme();5DOM binding
ThemeKit automatically writes CSS custom properties and a data-theme attribute to the target element. You can also read or apply them manually.
1// By default ThemeKit binds CSS variables to <html> automatically.
2// To target a specific element instead:
3const kit = new ThemeKit({ target: document.getElementById("app") });
4
5// Get a flat map of CSS custom properties for the current theme
6const vars = kit.toCSSVariables();
7// → { "--theme-color-primary": "#7c3aed", … }
8
9// Update individual tokens at runtime
10kit.update({ colors: { primary: "#10b981" } });What happens automatically |On every theme change, the target element gets
data-theme="…", color-scheme, and every --theme-* CSS custom property updated — no manual wiring needed.6Persistence
Mode and family are persisted to localStorage on every change and automatically restored when a new instance is created.
1// Persistence is automatic — mode and family are saved to
2// localStorage on every change and restored on init.
3
4// To write the current selection explicitly (useful if you
5// defer setup or change persistence strategy):
6kit.setMode("dark");
7kit.setFamily("plum");
8// ^ already persisted — no extra call needed
9
10// To access the persisted values before instantiating:
11const saved = JSON.parse(localStorage.getItem("theme-kit") ?? "{}");
12console.log(saved.mode, saved.family);7Full HTML example
A complete HTML page with a working theme switcher — save it as index.html and open it in a browser.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Theme Kit — Vanilla</title>
7 <style>
8 :root { color-scheme: light dark; }
9 body {
10 font-family: system-ui, sans-serif;
11 margin: 0;
12 padding: 2rem;
13 background: var(--theme-color-background);
14 color: var(--theme-color-foreground);
15 }
16 button {
17 padding: 0.5rem 1rem;
18 border-radius: 0.5rem;
19 border: 1px solid var(--theme-color-border);
20 background: var(--theme-color-card);
21 color: var(--theme-color-foreground);
22 cursor: pointer;
23 }
24 button:hover { border-color: var(--theme-color-primary); }
25 .active { border-color: var(--theme-color-primary); font-weight: 600; }
26 </style>
27</head>
28<body>
29 <h1>Theme Kit — Vanilla</h1>
30 <div id="controls" style="display:flex;gap:0.5rem;margin-bottom:1.5rem">
31 <button data-mode="light">Light</button>
32 <button data-mode="dark">Dark</button>
33 <button data-mode="system">System</button>
34 <button data-family="default">Default</button>
35 <button data-family="plum">Plum</button>
36 </div>
37 <p id="status"></p>
38
39 <script type="module">
40 import { ThemeKit } from "https://esm.sh/@theme-kit/core/vanilla";
41
42 const kit = ThemeKit.init({ defaultTheme: "light" });
43
44 const status = document.getElementById("status");
45 const render = () => {
46 status.textContent =
47 "Mode: " + kit.mode + " — Family: " + kit.family +
48 " — Theme: " + kit.theme.name;
49 document.querySelectorAll("[data-mode]").forEach((btn) => {
50 btn.classList.toggle("active", btn.dataset.mode === kit.mode);
51 });
52 document.querySelectorAll("[data-family]").forEach((btn) => {
53 btn.classList.toggle("active", btn.dataset.family === kit.family);
54 });
55 };
56
57 document.getElementById("controls").addEventListener("click", (e) => {
58 const btn = e.target.closest("button");
59 if (!btn) return;
60 if (btn.dataset.mode) kit.setMode(btn.dataset.mode);
61 if (btn.dataset.family) kit.setFamily(btn.dataset.family);
62 });
63
64 kit.on("themeChange", render);
65 render();
66 </script>
67</body>
68</html>