Skip to content

Core Concepts

@theme-kit/core

The mental model behind Theme Kit: semantic tokens, theme families, modes, the runtime, adapters, scoped themes and zero-flash SSR.

Theme

A theme is a named set of semantic tokens plus metadata. Themes are plain, serializable objects — no classes, no magic.

ts
1interface ThemeDefinition {
2  name: string;                // e.g. "plum-dark"
3  extends?: string | string[]; // inherit from another theme
4  meta?: ThemeMeta;            // label, family, mode, order, tags, version...
5  tokens?: ThemeTokens;        // colors, spacing, radius, shadows, ...
6}

A concrete theme looks like this:

ts
1import { defineTheme } from "@theme-kit/core";
2
3const plumDark = defineTheme({
4  name: "plum-dark",
5  meta: {
6    family: "plum",
7    mode: "dark",
8    label: "Plum Dark",
9    order: 20,
10    tags: ["preset"],
11  },
12  tokens: {
13    colors: {
14      background: "#171123",
15      foreground: "#f6f1fb",
16      primary: "#a78bfa",
17      card: "#221a33",
18      border: "#3b2d55",
19      // ... every semantic role
20    },
21    radius: { lg: "14px" },
22  },
23});

meta also carries created and updated timestamps (added automatically when a theme is registered), plus version for migrations.

Token Groups

Tokens are grouped by semantic meaning, not raw values:

  • colors — nested, recursively-addressable: colors.surface.default, colors.primary, colors.accent.hover
  • spacing, radius, shadows, borderWidths, zIndex, breakpoints
  • typographyfontFamilies, fontSizes, lineHeights

Because tokens are plain nested objects, you can address any leaf by its path — theme.tokens.colors.primary — and merge partial overrides safely.

Mode & Family

  • Mode"light" | "dark" | "system". system follows the OS with prefers-color-scheme.
  • Family — a named set of themes (e.g. "plum", "mint", "apple"). Switching family changes the palette; switching mode changes light/dark within that family.
ts
runtime.selection.setFamily("plum");  // plum light/dark pair
runtime.selection.setMode("dark");    // whichever plum is active, use dark
runtime.selection.toggleTheme();      // flip between light and dark

The active theme is always resolved from family + mode — the library picks the matching theme in the registry.

Semantic Token Groups

Nested tokens produce recursive groups that render as CSS variables:

text
--theme-color-surface-default
--theme-color-surface-hover
--theme-radius-lg
--theme-spacing-4

Every token maps to a --theme-* variable automatically via themeToCSSVariables:

ts
import { themeToCSSVariables } from "@theme-kit/core";

const vars = themeToCSSVariables(plumDark, { prefix: "theme-" });
// { "--theme-color-background": "#171123", "--theme-radius-lg": "14px", ... }

The Theme Store

A minimal reactive store that holds the active theme.

ts
1import { createThemeStore } from "@theme-kit/core";
2
3const store = createThemeStore({ initialTheme: myTheme });
4
5store.get();                                        // current theme
6store.set(nextTheme);                               // apply
7store.subscribe((theme) => render(theme));          // react to changes
8store.batch(() => { store.set(a); store.set(b); }); // coalesce writes
9store.destroy();

Model Builders

Type-safe, zero-cost helpers for composing themes:

ts
1import {
2  defineTheme,
3  extendTheme,
4  composeTheme,
5  mergeThemeDefinitions,
6  mergeTokens,
7  resolveTheme,
8} from "@theme-kit/core";
9
10const base = defineTheme({ name: "base", tokens: { ... } });
11
12// Inherit from a base theme and override pieces
13const plum = extendTheme("plum", base, {
14  meta: { family: "plum" },
15  tokens: { colors: { primary: "#a78bfa" } },
16});
17
18// Merge several definitions into one
19const full = composeTheme("full", base, plum, { tokens: { radius: { lg: "12px" } } });
20
21// Low-level merges
22const mergedTokens = mergeTokens(a.tokens, b.tokens);
23const mergedTheme = mergeThemeDefinitions(a, b);
24
25// Resolve a definition, following `extends` chains
26const resolved = resolveTheme([base, plum], "plum");

Theme Registry

Every registered theme lives in a registry — the engine behind dynamic theming, family lookups, and theme packs.

ts
1import { createThemeRegistry } from "@theme-kit/core";
2
3const registry = createThemeRegistry({ themes });
4
5registry.register(theme);
6registry.registerMany([a, b]);
7registry.unregister("plum-dark");
8registry.replace("plum-dark", newDark);
9registry.get("plum-light");
10registry.has("plum-light");
11registry.list();
12
13registry.getFamilies();                 // ["default", "oat", "plum", ...]
14registry.getThemesByFamily("plum");     // plum light + dark
15
16registry.use({ name: "brand", themes }); // install a theme pack

A theme pack is a named bundle of themes; every theme it installs is stamped with a pack:<name> tag in its meta.

The Theme Runtime

createThemeRuntime wires everything together — store, registry, selection, persistence, broadcast, DOM bindings, history, lifecycle, and plugins — into one object.

ts
1import { createThemeRuntime } from "@theme-kit/core";
2
3const runtime = createThemeRuntime({
4  themes,
5  defaultTheme: "light",
6  transition: { enabled: true, duration: 300 },
7  plugins: [createPersistencePlugin(), createHistoryPlugin()],
8});

Every capability is documented with snippets in the Architecture section. Key surfaces: store, registry, selection, themes, update, use, batch, snapshot/restore, history, lifecycle, destroy.

Token Resolution

Tokens support references and derived values, resolved lazily at runtime:

  • References"$colors.primary" or "{colors.primary}" point to another token (with circular-reference detection).
ts
{
  tokens: {
    colors: { primary: "#6366f1", ring: "$colors.primary" },
  },
}
  • Expressions — numeric math such as "calc(100% + 2rem)"-style evaluations:
ts
spacing: { "12": "calc(3rem + 0.75rem)" }
  • Derived colors"contrast(#123456)" returns black/white by WCAG luminance; "auto()" derives a foreground from a base token:
ts
colors: {
  primary: "#6366f1",
  primaryForeground: "auto()", // readable on primary
}

Utilities: flattenTokens, resolveFlatTokens, resolveTokens, hasTokenReferences, resolveValueReferences, evaluateExpression.

Theme Generation

Generate a complete light + dark theme pair from a single seed color, deriving secondary, muted, accent, border, and ring colors with HSL math.

ts
import { generateTheme } from "@theme-kit/core";

const { light, dark } = generateTheme({ seed: "#6366f1", family: "indigo" });

Theme Validation

Validate that a theme defines all required semantic color tokens, resolving extends chains when a theme list is provided.

ts
import { validateTheme } from "@theme-kit/core";

const result = validateTheme(theme, { themes });
// { valid: boolean, issues: [{ type: "missing", path, message }] }

Theme Migration

Version themes with a migration chain. Old theme files automatically upgrade to the latest format.

ts
1import { migrateTheme, registerMigration } from "@theme-kit/core";
2
3registerMigration({
4  from: "1.0.0",
5  to: "2.0.0",
6  remapColors: { primaryColor: "primary" },
7  migrate(theme) { /* arbitrary transforms */ },
8});
9
10const next = migrateTheme(theme, { targetVersion: "2.0.0" });

Theme History (Undo / Redo)

History is capped (default 50 steps) and records full theme snapshots with timestamps.

ts
runtime.history.undo();        // step back
runtime.history.redo();        // step forward
runtime.history.jump(i);       // jump to any point in time
runtime.history.canUndo();     // / canRedo()
runtime.history.getHistory();  // / clear()

Lifecycle Events

runtime.lifecycle.on(event, handler) with typed payloads:

EventPayload
beforeThemeChange{ current, next }
afterThemeChange{ theme }
beforePersist{ selection }
afterPersist{ selection }
beforeApply{ theme }
afterApply{ theme }
ts
const off = runtime.lifecycle.on("afterThemeChange", ({ theme }) => {
  console.log("applied", theme.name);
});

Plugins

Plugins hook into the lifecycle and can transform tokens.

ts
1interface ThemePlugin {
2  name: string;
3  priority?: number;
4  onBeforeThemeChange?(data): void;
5  onAfterThemeChange?(data): void;
6  onBeforePersist?(data): void;
7  onAfterPersist?(data): void;
8  onBeforeApply?(data): void;
9  onAfterApply?(data): void;
10  transformTokens?(tokens, ctx): ThemeTokens;
11}

Official plugins:

  • createPersistencePlugin() — persist selection to localStorage
  • createBroadcastPlugin() — cross-tab sync
  • createHistoryPlugin() — undo/redo
  • createAnimationsPlugin() — theme transition animation control
  • createAccessibilityPlugin() — contrast / accessibility enforcement
  • createScheduledPlugin() — auto light/dark by solar time
  • createDebuggerPlugin() — theme change logging
  • createDevToolsPlugin() — devtools inspector wiring
  • createGenerationPlugin() — live theme generation from a seed

Accessibility Toolkit

ts
1import {
2  getContrastRatio,
3  checkContrastPair,
4  validateThemeContrast,
5  simulateCVD,
6  simulateThemeForCVD,
7  getCVDLabel,
8} from "@theme-kit/core";
9
10const ratio = getContrastRatio("#ffffff", "#171123"); // 15.6...
11const ok = checkContrastPair("#fff", "#000", 4.5);     // WCAG check
12const audit = validateThemeContrast(theme);            // full theme audit
13
14// Color Vision Deficiency simulation (protanopia, deuteranopia, ...)
15const simulated = simulateCVD("#6366f1", "deuteranopia");

DOM Adapters

  • CSS Variables bindingcreateCSSVariablesBinding(store, { prefix, target, transition, styleSheet, layerName }). Writes --theme-* variables inline or into a @layer stylesheet; batches writes and diffs against previously applied variables for minimal DOM churn.
  • DOM Attribute bindingcreateDOMBinding(store, { target, attributeName, transition }). Sets data-theme, data-theme-family, data-theme-mode, toggles the .dark class, and sets color-scheme.
  • System theme bindingcreateSystemThemeBinding(store, { lightTheme, darkTheme }) — follows prefers-color-scheme.
  • Scoped theme bindingcreateScopedThemeBinding(themes, target, themeName) — apply a theme to a subtree.
  • TransitionsThemeTransitionOptions: enabled, duration, easing, useViewTransition, properties[] (40+ default animated properties).
  • View Transitions API — native document.startViewTransition when switching themes.
ts
1import { createCSSVariablesBinding, createDOMBinding } from "@theme-kit/core";
2
3const css = createCSSVariablesBinding(store, { prefix: "theme-" });
4const dom = createDOMBinding(store, {
5  attributeName: "data-theme",
6  transition: { enabled: true, duration: 300 },
7});

Multi-Window Sync

Sync theme selection across tabs/windows instantly:

  • BroadcastChannel — primary transport (createThemeSelectionBroadcast)
  • SharedWorker — inline blob-based worker relay
  • StorageEvent fallbackcreateStorageEventSync
  • Auto strategycreateMultiWindowSync({ prefer: "auto" | "broadcast" | "sharedworker" }) picks the best available transport and reports fallbacks
  • Zero-flicker — transitions are suppressed while applying cross-tab syncs
ts
import { createMultiWindowSync } from "@theme-kit/core";

const sync = createMultiWindowSync({ prefer: "auto" });
// choose the theme in one tab — every other tab follows instantly

Scheduled Themes (Solar Time)

Automatically switch between light and dark themes based on actual sunrise/sunset.

ts
1const runtime = createThemeRuntime({
2  scheduled: {
3    // Everything is optional. lightTheme/darkTheme adapt to the currently
4    // selected theme's family (fallback: neutral light/dark), and the
5    // location auto-detects from each visitor's browser timezone.
6    // lightTheme: "mint-light",
7    // darkTheme: "mint-dark",
8    // timeZone: "Asia/Kathmandu",
9    // latitude: 40.7128, longitude: -74.006,
10    checkInterval: 60_000,
11  },
12});

calculateSunTimes(date, lat?, lon?) computes NOAA solar events — with no coordinates it auto-detects the visitor's timezone (via Intl.DateTimeFormat().resolvedOptions().timeZone). When lightTheme / darkTheme are omitted, the schedule derives them from the current theme's family (e.g. plum-darkplum-light/plum-dark) and falls back to the neutral light/dark themes, re-resolving when the user switches family. timeZone / autoDetectLocation / latitude / longitude are all changeable at runtime with runtime.schedule.set(). skipApplyMs defers changes briefly after a cross-tab sync.

Persistence

  • createPersistencePlugin({ key }) — full selection persistence (mode + family) as a runtime plugin; default key theme-selection
  • createThemePersistence({ storage, key }) — mode-only persistence (theme-mode), returned as a ThemePersistenceAdapter for manual wiring (not directly assignable to the runtime persistence option)
  • ThemeSelectionPersistenceAdapter — the interface the runtime persistence option expects (get/set/remove/subscribe over { mode, family }); default runtime persistence stores { mode, family } under theme-selection in localStorage with a storage event subscription
ts
1import { createPersistencePlugin } from "@theme-kit/core";
2
3// Recommended: full selection (mode + family) persistence
4const runtime = createThemeRuntime({
5  themes,
6  plugins: [createPersistencePlugin({ key: "my-app-theme" })],
7});

Bootstrap (Zero Flash of Wrong Theme)

  • createThemeBootstrapScript({ themes, defaultTheme, initialMode, initialFamily, storageKey, prefix }) — generates a blocking inline script that reads the persisted selection, resolves the effective mode (systemprefers-color-scheme), and applies CSS variables + DOM effects before first paint.
  • buildThemeCssMap(themes) — maps theme names and family:mode keys to flat CSS variable maps.
  • darkModeCSSTemplate(variables) — a @media (prefers-color-scheme: dark) block so dark-mode users get correct colors even before JS runs.
ts
1import {
2  createThemeBootstrapScript,
3  buildThemeCssMap,
4  darkModeCSSTemplate,
5} from "@theme-kit/core";
6
7const cssMap = buildThemeCssMap(themes);
8const script = createThemeBootstrapScript({ themes, defaultTheme: "light" });
9const fallbackCSS = darkModeCSSTemplate(cssMap["default:dark"] ?? {});

Built-in Themes

getBuiltInThemes() bundles everything the library ships:

  • Neutrallight / dark with a full token scale (spacing, radius, shadows, border widths, z-index, breakpoints, typography)
  • Preset families — Oat, Berry, Mint, Citrus, Cocoa, Plum, Iris, Sky, Graphite (light + dark)
  • Brand presets — Apple, GitHub, Vercel, Slack, Discord (light + dark)
  • Accessibility profiles — High Contrast (light/dark) and Large Text (light/dark), tagged "accessibility"
ts
1import {
2  getBuiltInThemes,
3  getNeutralThemes,
4  getPresetThemes,
5  getBrandPresets,
6  getAccessibilityProfiles,
7} from "@theme-kit/core";
8
9const all = getBuiltInThemes();
10const presets = getPresetThemes();          // nine signature families
11const brands = getBrandPresets();           // five real-world brands

Vanilla (No Framework)

@theme-kit/core/vanilla provides the ThemeKit class — framework-free drop-in theming:

js
1import { ThemeKit } from "@theme-kit/core/vanilla";
2
3const kit = new ThemeKit();       // or ThemeKit.init()
4kit.setMode("dark");
5kit.setFamily("plum");
6kit.toggleTheme();
7kit.update({ colors: { primary: "#07f" } });
8kit.use({ name: "brand", themes: [...] });
9kit.toCSSVariables();
10kit.on("themeChange", (theme) => console.log(theme.name));
11kit.destroy();

Events: themeChange, modeChange, familyChange. Exposes .runtime, .registry, .theme, .mode, .family, .themes.

Vite Plugin

@theme-kit/core/vite injects the blocking bootstrap script into index.html (head-prepend) so the persisted theme applies before first paint in client-rendered apps — no manual inline scripts.

ts
1// vite.config.ts
2import { themeKitVitePlugin } from "@theme-kit/core/vite";
3
4export default defineConfig({
5  plugins: [react(), themeKitVitePlugin({ themes: customThemes })],
6});

Framework Integrations

The same runtime powers every framework integration:

  • @theme-kit/react — provider, hooks, ThemeScope, ThemeInspector, ThemeModeButton
  • @theme-kit/next — App Router SSR, cookies, zero-flash hydration
  • @theme-kit/vue, @theme-kit/svelte, @theme-kit/solid, @theme-kit/angular — provider + composables/stores/signals/injectables
  • @theme-kit/web<theme-kit-provider>, <theme-kit-toggle>, <theme-kit-select>, <theme-kit-scope> custom elements
  • @theme-kit/tailwind — Tailwind CSS v4 @theme mapping
  • @theme-kit/astro, @theme-kit/nuxt, @theme-kit/remix — island/SSR integrations
Core Concepts — Theme Kit