Skip to content

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 single entry point that wires every subsystem together.

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.

ts
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:

  1. Resolves the initial theme from defaultTheme / initialMode / initialFamily (or persisted selection).
  2. Registers every theme in the registry.
  3. Wires the store, selection controller, persistence, and broadcast.
  4. Applies DOM attributes and CSS variables via the bindings.

2Runtime API

Every feature hangs off the `runtime` object. Each entry below shows the property and a minimal usage snippet.

`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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
runtime.destroy();

3Layers

Each layer builds on the one below it.
1
Store
Minimal reactive state holding the active theme. `get`, `set`, `subscribe`, `batch`.
2
Registry
Every registered theme, powering dynamic theming, theme packs, and family lookups.
3
Selection
Mode + family resolution with persistence & broadcast, driving the store.
4
Adapters
CSS variables, DOM attributes, system theme, scoped themes, scheduled themes, transitions.
5
Resolvers
Token references, expressions, and derived colors resolved lazily at runtime.
6
Plugins
Hook into the lifecycle and transform tokens; official plugins ship for persistence, history, animations, accessibility, scheduling, debugging, and devtools.
7
Bootstrap
Blocking inline script for zero flash of incorrect theme, plus a `@media (prefers-color-scheme: dark)` fallback.

4How a Theme Change Flows

What happens when you call a method like `setFamily("plum")`.
text
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 snapshot

5Bootstrap: Zero Flash

A blocking inline script ensures the first paint is never the wrong theme.

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.

ts
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 paint

The @theme-kit/next provider does all of this automatically — it reads cookies on the server, renders the resolved theme, and emits the blocking script.

Architecture — Theme Kit