Architecture
@theme-kit/core — runtime, layers & data flow
Theme Kit's heart is a single framework-agnostic runtime that wires the store, registry, selection, persistence, broadcast, DOM bindings, history, lifecycle, and plugins together into one cohesive system.
1One Runtime
The heart of the library is a single framework-agnostic runtime that wires the store, registry, selection, persistence, broadcast, DOM bindings, history, lifecycle, and plugins together.
1import { createThemeRuntime } from "@theme-kit/core";
2
3const runtime = createThemeRuntime({
4 themes, // default: built-in themes
5 defaultTheme, // fallback theme name
6 initialMode, // "light" | "dark" | "system"
7 initialFamily, // e.g. "plum"
8 persistence, // localStorage adapter by default
9 broadcast, // BroadcastChannel adapter by default
10 dom, // DOM attribute binding options (false to disable)
11 cssVariables, // CSS variable binding options (false to disable)
12 transition, // smooth transitions
13 scheduled, // sunset/sunrise auto switching
14 plugins, // lifecycle + token transform plugins
15});Once created, the runtime immediately:
- Resolves the initial theme from
defaultTheme/initialMode/initialFamily(or persisted selection). - Registers every theme in the registry.
- Wires the store, selection controller, persistence, and broadcast.
- Applies DOM attributes and CSS variables via the bindings.
2Runtime API
`runtime.store`
The reactive store holding the active theme. `get()` reads it, `set()` replaces it, `subscribe()` reacts to changes, and `batch()` coalesces multiple writes into a single notification.
1const theme = runtime.store.get(); // current theme
2const unsubscribe = runtime.store.subscribe((t) => {
3 console.log("theme changed:", t.name);
4});
5
6runtime.store.set(myTheme); // apply immediately
7runtime.store.batch(() => { // coalesce several writes
8 runtime.store.set(a);
9 runtime.store.set(b);
10});
11unsubscribe();`runtime.registry`
Every registered theme, powering dynamic theming. Register, unregister, replace, look up, and group themes by family.
1runtime.registry.register(myTheme); // add a theme
2runtime.registry.registerMany([a, b, c]); // add several
3runtime.registry.unregister("plum-dark"); // remove by name
4runtime.registry.replace("plum-dark", newDark); // swap in place
5runtime.registry.get("plum-light"); // find by name
6runtime.registry.has("plum-light"); // boolean
7runtime.registry.list(); // all themes
8runtime.registry.getFamilies(); // ["default", "plum", ...]
9runtime.registry.getThemesByFamily("plum"); // plums only`runtime.selection`
Mode + family resolution. Handles persistence and broadcast for you, and keeps the store in sync.
runtime.selection.setMode("dark"); // "light" | "dark" | "system"
runtime.selection.setFamily("mint"); // switch palette
runtime.selection.toggleTheme(); // flip light <-> dark
runtime.selection.getSelection();
// { mode: "dark", family: "mint" }`runtime.themes`
A read-only list of all registered themes — handy for rendering pickers, galleries, or walking families.
1for (const theme of runtime.themes) {
2 console.log(theme.name, theme.meta?.family, theme.meta?.mode);
3}
4
5const plums = runtime.themes.filter(
6 (t) => t.meta?.family === "plum",
7);`runtime.update(tokens)`
Live theme editing: merge partial tokens into the active theme and re-apply. The perfect primitive for theme studios and design-time tweaking.
1runtime.update({
2 colors: {
3 primary: "#6366f1",
4 accent: { hover: "#4f46e5" },
5 },
6 radius: { lg: "16px" },
7});`runtime.use(pack)`
Install a theme pack at runtime. A pack is a named bundle of themes; every theme in it is stamped with a `pack:<name>` tag.
1runtime.use({
2 name: "brand",
3 themes: [
4 { name: "apple-light", meta: { family: "apple", mode: "light" }, tokens },
5 { name: "apple-dark", meta: { family: "apple", mode: "dark" }, tokens },
6 ],
7});`runtime.batch(cb)`
Run a callback atomically — intermediate state changes are suppressed until the callback finishes.
1runtime.batch(() => {
2 runtime.update({ colors: { primary: "#000" } });
3 runtime.selection.setMode("dark");
4 runtime.selection.setFamily("plum");
5});
6// subscribers fire exactly once, with the final theme`runtime.snapshot()` / `runtime.restore(snapshot)`
Serialize the full runtime state — theme, selection, history, and registry — and restore it later. Ideal for time travel and demo replay.
1const snapshot = runtime.snapshot();
2// { theme, selection, history: [], registry: { themes } }
3
4// ... make changes ...
5
6runtime.restore(snapshot); // back to exactly how it was`runtime.history`
Built-in undo/redo, capped at 50 steps by default. Records full theme snapshots with timestamps.
1runtime.history.undo(); // step back
2runtime.history.redo(); // step forward
3runtime.history.jump(2); // jump to any point in time
4runtime.history.canUndo(); // boolean
5runtime.history.canRedo(); // boolean
6runtime.history.getHistory(); // HistoryEntry[] { theme, timestamp }
7runtime.history.clear(); // wipe the timeline`runtime.lifecycle`
A typed event bus for the theme pipeline. Subscribe with `on()` (returns an unsubscribe function) and react to typed payloads.
1const off = runtime.lifecycle.on("beforeThemeChange", ({ current, next }) => {
2 console.log(`leaving ${current.name}, entering ${next.name}`);
3});
4
5runtime.lifecycle.on("afterApply", ({ theme }) => {
6 document.title = `Theme — ${theme.name}`;
7});
8
9off(); // unsubscribe`runtime.destroy()`
Full teardown: unsubscribes every listener, removes DOM/CSS bindings, closes the broadcast channel, and clears registry, history, and lifecycle.
runtime.destroy();3Layers
4How a Theme Change Flows
1setFamily("plum")
2 → selection.setFamily()
3 → persistence.set({ mode, family }) // save
4 → broadcast.post({ mode, family }) // sync other tabs
5 → resolve theme for family + mode
6 → lifecycle.emit("beforeThemeChange")
7 → plugins.onBeforeThemeChange
8 → store.set(theme)
9 → lifecycle.emit("afterThemeChange")
10 → DOM binding: data-theme, data-theme-mode, .dark class
11 → CSS variables binding: --theme-* variables
12 → lifecycle.emit("beforeApply" / "afterApply")
13 → history records a snapshot5Bootstrap: Zero Flash
Before any JS runs, a blocking inline script reads the persisted selection, resolves the effective mode (system → prefers-color-scheme), and applies CSS variables + DOM effects.
1import { createThemeBootstrapScript, buildThemeCssMap } from "@theme-kit/core";
2
3const cssMap = buildThemeCssMap(themes); // name + family:mode → variables
4const script = createThemeBootstrapScript({
5 themes,
6 defaultTheme: "light",
7 initialMode: "system",
8 storageKey: "theme-selection",
9 prefix: "theme-",
10});
11// inject `script` into <head> before first paintThe @theme-kit/next provider does all of this automatically — it reads cookies on the server, renders the resolved theme, and emits the blocking script.