Rendering blocks standalone
The doc blocks render from a serializable snapshot of the token project, so any React tree can host them: a docs site, a design-system portal, a test suite. No Storybook, no addon. The work splits in two: a Node build step produces the snapshot, and the app applies its CSS and mounts the provider.
Produce the snapshot
Section titled “Produce the snapshot”Four calls at build time: load the project, emit its CSS, reduce both to the wire shape, write JSON.
import { writeFile } from 'node:fs/promises';import { loadProject, emitAxisProjectedCss } from '@unpunnyfuns/swatchbook-core';import { snapshotForWire } from '@unpunnyfuns/swatchbook-core/snapshot-for-wire';import config from '../swatchbook.config.ts';
const project = await loadProject(config, process.cwd());const css = emitAxisProjectedCss(project);const wire = snapshotForWire(project, css);await writeFile('src/tokens-snapshot.json', JSON.stringify(wire));snapshotForWire strips the parts of Project that don’t survive JSON.stringify and keeps what the blocks read: axes, token graph, listing, diagnostics, and the emitted CSS string. The core reference covers the shape.
Wire it in as a pre-step of the app build. Node 24 executes TypeScript directly, so the script runs as-is:
{ "scripts": { "prebuild": "node scripts/tokens-snapshot.mts", "build": "vite build" }}Mount it
Section titled “Mount it”Wrap the blocks in the provider; it mounts wire.css itself, so the per-tuple custom-property blocks every block visual reads through var() are defined with no further wiring (Token CSS covers the opt-out).
import { ColorPalette, SwatchbookProvider, TokenTable } from '@unpunnyfuns/swatchbook-blocks';import wire from './tokens-snapshot.json';
export function TokenDocs() { return ( <SwatchbookProvider snapshot={wire} defaultAxes={{ mode: 'Light' }}> <ColorPalette filter="color.**" /> <TokenTable filter="space.**" /> </SwatchbookProvider> );}The provider resolves every token at the active tuple, scopes the axis data attributes to its own wrapper element rather than <html> (two providers with different tuples coexist on one page), and merges any presenters overrides over the built-in registry. Props and the host seam are in the Provider reference; Token CSS covers the stylesheet responsibility in detail.
Flip axes
Section titled “Flip axes”Uncontrolled: useSetAxes()
Section titled “Uncontrolled: useSetAxes()”With defaultAxes, the provider owns the tuple and descendants flip it through useSetAxes(). A minimal switcher reads the project’s axes from useSwatchbookData() and renders a <select> per axis. The setter replaces the whole tuple rather than merging, so spread the current tuple when changing one axis:
import { useSetAxes, useSwatchbookData } from '@unpunnyfuns/swatchbook-blocks';
export function AxisSelects() { const { axes, activeAxes } = useSwatchbookData(); const setAxes = useSetAxes(); return ( <> {axes.map((axis) => ( <label key={axis.name}> {axis.name} <select value={activeAxes[axis.name]} onChange={(event) => setAxes({ ...activeAxes, [axis.name]: event.target.value })} > {axis.contexts.map((context) => ( <option key={context}>{context}</option> ))} </select> </label> ))} </> );}Render it anywhere inside the provider; every block under the same provider repaints on change.
Controlled: host-owned state
Section titled “Controlled: host-owned state”When the theme selection already lives in the host (a global store, a router param, an existing theme context), pass axes instead and re-render the provider on change:
const [axes, setAxes] = useState(wire.defaultTuple);
<SwatchbookProvider snapshot={wire} axes={axes}> <TokenTable filter="color.**" /></SwatchbookProvider>;Under a controlled provider useSetAxes() throws: there is no internal state to flip.
Ready-made switcher
Section titled “Ready-made switcher”@unpunnyfuns/swatchbook-switcher ships ThemeSwitcher, the same menu the Storybook toolbar mounts: preset pills, one row per axis, tuple application left to the caller. This docs site uses it as the header theme control. The Switcher reference has the wiring.
What you give up
Section titled “What you give up”The snapshot is a build artifact. Edits to token files reach the page on the next snapshot regeneration and reload; there is no HMR push. Diagnostics still ship in the snapshot, so the Diagnostics block works unchanged. Storybook’s authoring surface stays behind: autodocs, stories, the toolbar.
See also
Section titled “See also”- Presenter registry: overriding how a
$typerenders inside the blocks. - Consuming the active theme: CSS-variable and DOM-attribute consumption for your own components.