Skip to content

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.

Token source files are parsed, built into a graph, snapshotted, and shown in the preview as CSS variables and doc blocks. Saving a source file re-runs the whole pipeline in memory.runs in memory; nothing is written to diskToken sourcesDTCG filesParseBuild graphSnapshotPreviewCSS vars + blockssaving a source file re-runs the pipeline

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.

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.

At load time, sources are parsed, aliases resolved, and axis effects indexed into the token graph, once. At display time, blocks query the graph for values, aliases, and variance and get instant answers; nothing on the display side reaches back into parsing.At load: onceAt display: constantlyParse sourcesResolve aliasesIndex axis effectsToken graphvalue at this tuple?what does this alias?which axes vary this?expensive work, done onceinstant answers, every render

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.

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.

One project snapshot feeds the Storybook preview, doc blocks outside Storybook, and the MCP server.Project snapshotcomputed onceStorybook previewthe addonDocs-site blocksoutside StorybookMCP serverAI agents
  • Storybook preview: the primary case.
  • Docs site blocks outside Storybook: the blocks package works outside Storybook given a ProjectSnapshot through SwatchbookProvider. 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-mcp loads the project directly via loadProject at startup, exposes its tools against the in-memory graph, watches the same source files for live reload. No intermediate build step.
  • 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.watch that keeps atomic-save editors working are covered under Sharp corners.