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?
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.
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
onRuntimeCreatedCalled once when the runtime finishes initialization.
onBeforeThemeChangeFires before the active theme switches.
onAfterThemeChangeFires after the new theme is active.
onBeforePersistFires before the selection is saved.
onAfterPersistFires after the selection has been persisted.
onBeforeApplyFires before CSS variables are written to the DOM.
onAfterApplyFires after CSS variables have been applied.
onDestroyCalled when the runtime is destroyed.
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};3Token Transforms
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.
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
prioritydefaults to10when omitted.- Lower values execute first — a plugin with priority
0runs before one with10. - Plugins with the same priority run in registration order.
transformTokenshooks chain in priority order — each plugin sees the previous output.
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
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.
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();6Full Example
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});