Skip to content

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.
basic setup
ts
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.
reading state
ts
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;
subscribing to changes
ts
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.
switching themes
ts
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.
manual CSS variable binding
ts
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.
persistence
ts
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.
full HTML page
html
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>
Framework-Free — Theme Kit