The token pipeline
Swatchbook doesn't require a separate token build step. Tokens flow into the preview through a Vite virtual module the addon publishes at dev-server start, updated via HMR on every source file change. No generated CSS in your repo, no predev script, no config to keep in sync.
A virtual module, not a build artifact
The addon's Vite plugin publishes a virtual ESM module named virtual:swatchbook/tokens. The Storybook preview imports from it like any other module:
import { tokenGraph, defaultTuple, axes, diagnostics, css } from 'virtual:swatchbook/tokens';
"Virtual" means there's no file on disk. Vite asks the plugin to materialize the module on each preview build, and the plugin does this by calling loadProject(config, cwd) from @unpunnyfuns/swatchbook-core and serializing the result into ESM exports:
swatchbook.config.ts ← your config
↓
Vite plugin buildStart
↓
loadProject(config, cwd) ← parses DTCG, builds token graph, emits CSS
↓
Project snapshot
↓
Vite plugin load('virtual:swatchbook/tokens')
↓
export const tokenGraph = { … }
export const defaultTuple = { … }
export const axes = [ … ]
export const css = '…' ← preview decorator injects as <style>
Nothing written to disk. Edit your config or a token file, Vite notices, the plugin re-runs loadProject, preview sees fresh values over HMR.
HMR
The Vite plugin watches every file your config loaded (the resolver, every $ref target, every token file the globs matched) and triggers a reload on any change. Reload is cheap (one loadProject call and one module invalidation, not a full-page reload), so the preview re-renders in place without losing toolbar state, story args, or scroll position.
A short-form HMR event rides Storybook's channel to push the fresh snapshot into running blocks. Token graph updates in under a second on a typical save; edit a token with the live Storybook open to see it.
See consuming the active theme for how stories react to axis flips: related but distinct plumbing.
Not a production theming API
The virtual module lives inside Vite's module graph: the Storybook preview resolves it, consumer apps (Next.js, a Vite SPA, Remix) don't. For production, run Terrazzo's CLI against the same DTCG sources; its plugin ecosystem (plugin-css, plugin-js, plugin-tailwind, plugin-swift, plugin-sass, …) covers downstream platforms. When both pipelines run, align their options so what swatchbook shows matches what your apps ship.
Where the same property holds
- Storybook preview: the primary case.
- Docs site blocks outside Storybook: the blocks package works outside Storybook given a
ProjectSnapshotthroughSwatchbookProvider. This docs site does that: a small build step computes the snapshot JSON once and ships it as a regular import. - MCP server:
@unpunnyfuns/swatchbook-mcploads the project directly vialoadProjectat startup, exposes its tools against the in-memory graph, watches the same source files for live reload. No intermediate build step.
Sharp edges
- The virtual module is addon-internal plumbing. Its shape, export set, and event names can change between minor versions without a changeset entry. Consumers shouldn't import from
virtual:swatchbook/tokensdirectly; useSwatchbookProvider+ the exported hooks. - Vite is required. The addon bundles the plugin for Storybook's Vite integration. Webpack-based Storybook setups don't get the virtual module materialized; they would fall back to a pre-built snapshot or switch to the Vite builder. Storybook 10's default is Vite, so this only bites older setups.
- File-system edits flow through a dir-level
fs.watch(not a file-level watcher) so atomic-save editors survive.