Skip to content

Plugins

Extend Theme Kit with plugins

Theme Kit's plugin system lets you hook into every stage of the theme lifecycle — from runtime creation to token transforms to persistence. Plugins are small, composable objects that follow a single ThemePlugin interface.

1What is a Plugin?

A plugin is an object with a name, an optional version and priority, and any combination of lifecycle hooks.

Every plugin implements the ThemePlugin interface. At minimum, a plugin needs a name — everything else is optional. Plugins are registered when the runtime is created and can be managed dynamically via the PluginManager.

defining a basic plugin
ts
1import type { ThemePlugin } from "@theme-kit/core";
2
3const myPlugin: ThemePlugin = {
4  name: "my-plugin",
5  version: "1.0.0",
6  priority: 10,
7
8  onRuntimeCreated(runtime) {
9    console.log("Runtime ready", runtime);
10  },
11
12  onAfterThemeChange({ theme }) {
13    console.log("Theme changed to", theme.name);
14  },
15};

2Plugin Hooks

Lifecycle hooks fire at predictable points during the theme runtime's lifetime.
onRuntimeCreated

Called once when the runtime finishes initialization.

onBeforeThemeChange

Fires before the active theme switches.

onAfterThemeChange

Fires after the new theme is active.

onBeforePersist

Fires before the selection is saved.

onAfterPersist

Fires after the selection has been persisted.

onBeforeApply

Fires before CSS variables are written to the DOM.

onAfterApply

Fires after CSS variables have been applied.

onDestroy

Called when the runtime is destroyed.

plugin hooks
ts
1import type { ThemePlugin } from "@theme-kit/core";
2
3const logger: ThemePlugin = {
4  name: "logger",
5
6  onRuntimeCreated(runtime) {
7    // Called once when createThemeRuntime() finishes.
8    // Return a teardown function to run on destroy().
9    return () => console.log("runtime destroyed");
10  },
11
12  onBeforeThemeChange({ current, next }) {
13    // Fires before the active theme switches.
14    console.log(`${current.name} → ${next.name}`);
15  },
16
17  onAfterThemeChange({ theme }) {
18    // Fires after the new theme is active.
19  },
20
21  onBeforePersist({ selection }) {
22    // Before the selection is saved to the persistence adapter.
23  },
24
25  onAfterPersist({ selection }) {
26    // After the selection has been persisted.
27  },
28
29  onBeforeApply({ theme }) {
30    // Before CSS variables are written to the DOM.
31  },
32
33  onAfterApply({ theme }) {
34    // After CSS variables have been applied.
35  },
36
37  onDestroy() {
38    // Cleanup when the runtime is destroyed.
39  },
40};
onRuntimeCreated can return a teardown |If the hook returns a function, it will be called when the runtime is destroyed — useful for cleaning up subscriptions.

3Token Transforms

Transform tokens before they reach the DOM — override, augment, or remap any token path.

The transformTokens hook receives the full token object and the current theme, and must return the (potentially modified) tokens. Multiple transform plugins chain in priority order — each receives the output of the previous.

token transform plugin
ts
1import type { ThemePlugin, ThemeTokens } from "@theme-kit/core";
2
3const contrastBoost: ThemePlugin = {
4  name: "contrast-boost",
5  priority: 5,
6
7  transformTokens(tokens, { theme }) {
8    // Adjust foreground tokens for better contrast.
9    if (theme.meta?.mode === "dark") {
10      return {
11        ...tokens,
12        colors: {
13          ...tokens.colors,
14          foreground: adjustBrightness(tokens.colors.foreground, 10),
15        },
16      };
17    }
18    return tokens;
19  },
20};

4Priority & Ordering

The priority field controls plugin execution order — lower values run first.
  • priority defaults to 10 when omitted.
  • Lower values execute first — a plugin with priority 0 runs before one with 10.
  • Plugins with the same priority run in registration order.
  • transformTokens hooks chain in priority order — each plugin sees the previous output.
priority ordering
ts
1import type { ThemePlugin } from "@theme-kit/core";
2
3const pluginA: ThemePlugin = {
4  name: "a",
5  priority: 0,   // Runs first
6  transformTokens(tokens) { /* … */ return tokens; },
7};
8
9const pluginB: ThemePlugin = {
10  name: "b",
11  // priority defaults to 10
12  transformTokens(tokens) { /* … */ return tokens; },
13};
14
15const pluginC: ThemePlugin = {
16  name: "c",
17  priority: 20,  // Runs last
18  transformTokens(tokens) { /* … */ return tokens;
19};
20
21// Execution order: A → B → C
22// Lower priority values execute first.
23// Plugins with the same priority run in registration order.

5Runtime Integration

Manage plugins at runtime with the PluginManager — register, look up, remove, and destroy.

Plugins are passed at runtime creation via the plugins option. For dynamic management, use createPluginManager() to create a standalone manager, or call runtime.destroy() to tear down all registered plugins.

runtime plugin management
ts
1import { createThemeRuntime, createPluginManager } from "@theme-kit/core";
2
3const runtime = createThemeRuntime({
4  plugins: [debuggerPlugin, persistencePlugin],
5});
6
7// Standalone plugin manager
8const manager = createPluginManager();
9
10// Register a plugin — returns an unsubscribe function
11const unsubscribe = manager.use(myPlugin);
12
13// Look up a registered plugin by name
14const found = manager.get("my-plugin");
15
16// Remove a plugin by name
17manager.remove("my-plugin");
18
19// List all registered plugins (sorted by priority)
20const all = manager.list();
21
22// Destroy all plugins and call their onDestroy hooks
23manager.destroy();
24
25// Unsubscribe to remove the plugin later
26unsubscribe();
Duplicate names are skipped |Registering a plugin with a name that is already registered logs a warning and returns a no-op unsubscribe function.

6Full Example

A complete plugin that dispatches custom events on theme changes and boosts contrast on dark tokens.
full plugin example
ts
1import type { ThemePlugin, ThemeTokens } from "@theme-kit/core";
2
3/**
4 * Logs every theme change and boosts contrast on dark tokens.
5 */
6const analyticsPlugin: ThemePlugin = {
7  name: "theme-analytics",
8  version: "1.0.0",
9  priority: 5,
10
11  onRuntimeCreated(runtime) {
12    const unsubscribe = runtime.store.subscribe((theme) => {
13      window.dispatchEvent(
14        new CustomEvent("theme-change", { detail: theme.name }),
15      );
16    });
17    return unsubscribe;
18  },
19
20  onBeforeThemeChange({ current, next }) {
21    console.log(
22      `[theme-analytics] ${current.name} → ${next.name}`,
23    );
24  },
25
26  onAfterApply({ theme }) {
27    console.log(`[theme-analytics] Applied ${theme.name}`);
28  },
29
30  transformTokens(tokens: ThemeTokens, { theme }) {
31    if (theme.meta?.mode === "dark") {
32      return {
33        ...tokens,
34        colors: {
35          ...tokens.colors,
36          foreground: tokens.colors.foreground ?? "#f5f5f5",
37        },
38      };
39    }
40    return tokens;
41  },
42
43  onDestroy() {
44    console.log("[theme-analytics] Destroyed");
45  },
46};
47
48// Pass to the runtime at creation
49import { createThemeRuntime } from "@theme-kit/core";
50
51const runtime = createThemeRuntime({
52  plugins: [analyticsPlugin],
53});
Plugins — Theme Kit