DOM Adapters
Bridging runtime and browser
DOM adapters are the bridge between Theme Kit's runtime store and the live document. They translate theme definitions into CSS variables, HTML attributes, and media-query listeners — keeping the core logic framework-agnostic while the adapters handle the browser.
1What are DOM Adapters?
- Every adapter receives a
ThemeStoreand callsstore.subscribe()to react to theme changes. - Adapters are composable — you can use CSS variables binding without DOM attributes, or system binding without scoped themes.
- Each adapter returns a
destroy()function that unsubscribes and cleans up any injected DOM nodes. - The CSS-variables binding owns the transition pipeline (diff → plan → animate). Other bindings opt in by setting
subscribe: falseand receiving theapplycallback through the shared View Transition.
2CSS Variables Binding
This is the primary adapter. It converts every token in a theme definition into a CSS custom property and keeps it synchronized. The default prefix theme- produces variables like --theme-color-primary and --theme-radius-lg.
1import { createThemeRuntime } from "@theme-kit/core";
2import { createCSSVariablesBinding } from "@theme-kit/core";
3
4const runtime = createThemeRuntime({
5 themes,
6 defaultTheme: "light",
7});
8
9// Writes --theme-color-* custom properties to :root.
10// Each theme token becomes a CSS variable automatically.
11const cssVars = createCSSVariablesBinding(runtime.store, {
12 prefix: "theme-", // default prefix
13 target: document.documentElement,
14 styleSheet: false, // inline <style> or element.style
15 transition: {
16 enabled: true,
17 duration: 360,
18 easing: "cubic-bezier(0.4, 0, 0.2, 1)",
19 useViewTransition: true,
20 },
21});
22
23// A theme switch now produces:
24// --theme-color-primary: #6366f1
25// --theme-color-background: #ffffff
26// --theme-color-foreground: #171717
27// …every token in the theme definition.@property so the browser can interpolate between values — enabling smooth CSS-native transitions without per-element transition styles.3DOM Attribute Binding
While CSS variables deliver color values, the DOM binding delivers identity. It sets the data-theme, data-theme-family, and data-theme-mode attributes, toggles the dark class, and writes the color-scheme CSS property so native form controls and scrollbars match the theme.
1import { createDOMBinding } from "@theme-kit/core";
2
3const dom = createDOMBinding(runtime.store, {
4 target: document.documentElement,
5 attributeName: "data-theme", // default
6});
7
8// On every theme change the binding writes:
9// data-theme="plum-dark"
10// data-theme-family="plum"
11// data-theme-mode="dark"
12// class="dark" ← toggled on/off
13// color-scheme: dark ← via inline style4System Theme Binding
The system binding watches prefers-color-scheme via matchMedia and updates the store when the OS switches between light and dark. Downstream bindings (CSS variables, DOM attributes) react automatically — no wiring needed.
1import { createSystemThemeBinding } from "@theme-kit/core";
2
3const system = createSystemThemeBinding(runtime.store, {
4 lightTheme: themes.find(t => t.name === "light")!,
5 darkTheme: themes.find(t => t.name === "dark")!,
6 mediaQuery: "(prefers-color-scheme: dark)",
7});
8
9// The binding listens for OS preference changes.
10// When the user switches system appearance:
11// - "prefers-color-scheme: dark" matches → store.set(darkTheme)
12// - "prefers-color-scheme: light" matches → store.set(lightTheme)
13//
14// Downstream bindings (CSS vars, DOM attrs) react automatically.system mode during SSR using a @media (prefers-color-scheme: dark) fallback, so the first paint already matches the OS — no flash.5Scoped Theme Binding
Scoped bindings apply theme variables and attributes to a specific element rather than :root. Child elements inherit the scoped variables, so a sidebar can be permanently dark while the page stays light. The binding supports local theme definitions, transition inheritance from the parent runtime, and clean teardown.
1import { createScopedThemeBinding } from "@theme-kit/core";
2
3const sidebar = document.getElementById("sidebar")!;
4
5const scope = createScopedThemeBinding(
6 themes,
7 sidebar,
8 "plum-dark", // or { family: "plum", mode: "dark" }
9 {
10 prefix: "theme-",
11 transition: { enabled: true, duration: 200 },
12 localThemes: [customTokenPack], // optional local overrides
13 },
14);
15
16// The scope element receives:
17// data-theme="plum-dark"
18// data-mode="dark"
19// class="dark"
20// --theme-color-* inline variables
21// --color-* aliases (Tailwind-style)
22//
23// Child elements inherit scoped variables — the page theme is
24// completely isolated from this subtree.
25
26scope.update("plum-light"); // animate to a new selection
27scope.setTransition({ duration: 0 }); // instant swap
28scope.destroy(); // clean up variables + attrs6Transition Binding
Every animated theme change flows through three stages:
1import {
2 createThemeDiff,
3 createTransitionPlan,
4 runThemeAnimation,
5 cancelThemeAnimation,
6} from "@theme-kit/core";
7
8// 1. Diff: compare old vs new variable maps
9const diff = createThemeDiff(appliedVariables, newVariables);
10
11// 2. Plan: classify changed variables into animated groups
12// (colors, radii, shadows) and compute per-group timing.
13const plan = createTransitionPlan(diff, {
14 enabled: true,
15 duration: 360,
16 easing: "cubic-bezier(0.4, 0, 0.2, 1)",
17}, { reducedMotion: false });
18
19// 3. Animate: apply the plan to the target element.
20if (plan) {
21 runThemeAnimation({
22 target: document.documentElement,
23 plan,
24 swap: () => {
25 // Apply new CSS variables
26 applyInlineVariables(element, newVariables);
27 },
28 });
29}
30
31// The CSS-variables binding owns this entire pipeline.
32// You don't call these directly — they run inside
33// createCSSVariablesBinding's store subscriber.document.startViewTransition() — a single cross-fade that paints the new theme beneath a snapshot of the old one, eliminating white-shift on light→dark switches.7Disabling Adapters
Not every app needs every adapter. The runtime accepts a dom: false flag to skip attribute binding at the provider level, and individual bindings accept options to disable transitions or unsubscribe from the store.
1// Disable DOM attribute binding entirely
2const runtime = createThemeRuntime({
3 themes,
4 defaultTheme: "light",
5 dom: false, // no data-theme, no data-theme-family, no .dark class
6});
7
8// Disable CSS variable output
9const cssVars = createCSSVariablesBinding(runtime.store, {
10 styleSheet: true,
11 layerName: "theme-kit",
12});
13// To skip CSS variables entirely, don't call this binding.
14
15// Disable transitions on a specific binding
16const dom = createDOMBinding(runtime.store, {
17 transition: { enabled: false }, // instant attribute swap
18});
19
20// Combine: attributes + variables, no transition
21const dom = createDOMBinding(runtime.store, {
22 subscribe: false, // driven by CSS-variables binding's pipeline
23});
24const cssVars = createCSSVariablesBinding(runtime.store, {
25 transition: { enabled: false },
26});