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
Section titled “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.
Edit a token or resolver file, Vite notices, the plugin re-runs loadProject, and the preview sees fresh values over HMR.
Build once, query fast
Section titled “Build once, query fast”Parsing DTCG sources, resolving alias chains, and working out which axes affect which token is the expensive part. All of it happens once, at load, and the results are folded into a walkable token graph inside the snapshot.
Everything interactive reads from that graph: token tables, alias inspectors, the switcher’s repaints. Those queries are lookups, not resolution, so they stay fast regardless of how many axes or aliases a system declares. The display side never reaches back into parsing.
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
Section titled “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. swatchbook’s place in the pipeline is preview host, not transformer; for production, run Terrazzo’s CLI against the same DTCG sources and align the options so what swatchbook shows matches what your apps ship.
Where the same property holds
Section titled “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
Section titled “Sharp edges”- 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.
- The virtual module’s addon-internal status and the directory-level
fs.watchthat keeps atomic-save editors working are covered under Sharp corners.