Plugins
Extend Themvora with plugins
Themvora'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.
import type { ThemePlugin } from "@themvora/core";
const myPlugin: ThemePlugin = {
name: "my-plugin",
version: "1.0.0",
priority: 10,
onRuntimeCreated(runtime) {
console.log("Runtime ready", runtime);
},
onAfterThemeChange({ theme }) {
console.log("Theme changed to", theme.name);
},
};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.
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.
import type { ThemePlugin, ThemeTokens } from "@themvora/core";
const contrastBoost: ThemePlugin = {
name: "contrast-boost",
priority: 5,
transformTokens(tokens, { theme }) {
// Adjust foreground tokens for better contrast.
if (theme.meta?.mode === "dark") {
return {
...tokens,
colors: {
...tokens.colors,
foreground: adjustBrightness(tokens.colors.foreground, 10),
},
};
}
return tokens;
},
};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.
import type { ThemePlugin } from "@themvora/core";
const pluginA: ThemePlugin = {
name: "a",
priority: 0, // Runs first
transformTokens(tokens) { /* … */ return tokens; },
};
const pluginB: ThemePlugin = {
name: "b",
// priority defaults to 10
transformTokens(tokens) { /* … */ return tokens; },
};
const pluginC: ThemePlugin = {
name: "c",
priority: 20, // Runs last
transformTokens(tokens) { /* … */ return tokens;
};
// Execution order: A → B → C
// Lower priority values execute first.
// 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.
import { createThemeRuntime, createPluginManager } from "@themvora/core";
const runtime = createThemeRuntime({
plugins: [debuggerPlugin, persistencePlugin],
});
// Standalone plugin manager
const manager = createPluginManager();
// Register a plugin — returns an unsubscribe function
const unsubscribe = manager.use(myPlugin);
// Look up a registered plugin by name
const found = manager.get("my-plugin");
// Remove a plugin by name
manager.remove("my-plugin");
// List all registered plugins (sorted by priority)
const all = manager.list();
// Destroy all plugins and call their onDestroy hooks
manager.destroy();
// Unsubscribe to remove the plugin later
unsubscribe();6Full Example
import type { ThemePlugin, ThemeTokens } from "@themvora/core";
/**
* Logs every theme change and boosts contrast on dark tokens.
*/
const analyticsPlugin: ThemePlugin = {
name: "theme-analytics",
version: "1.0.0",
priority: 5,
onRuntimeCreated(runtime) {
const unsubscribe = runtime.store.subscribe((theme) => {
window.dispatchEvent(
new CustomEvent("theme-change", { detail: theme.name }),
);
});
return unsubscribe;
},
onBeforeThemeChange({ current, next }) {
console.log(
`[theme-analytics] ${current.name} → ${next.name}`,
);
},
onAfterApply({ theme }) {
console.log(`[theme-analytics] Applied ${theme.name}`);
},
transformTokens(tokens: ThemeTokens, { theme }) {
if (theme.meta?.mode === "dark") {
return {
...tokens,
colors: {
...tokens.colors,
foreground: tokens.colors.foreground ?? "#f5f5f5",
},
};
}
return tokens;
},
onDestroy() {
console.log("[theme-analytics] Destroyed");
},
};
// Pass to the runtime at creation
import { createThemeRuntime } from "@themvora/core";
const runtime = createThemeRuntime({
plugins: [analyticsPlugin],
});7API reference
The full surface, generated from source JSDoc: