Skip to content

Addon

Wiring swatchbook into Storybook is this package’s job. It loads your config at build time through @unpunnyfuns/swatchbook-core, exposes the resolved graph as a virtual module, and at preview time registers a decorator and a manager toolbar, and exposes the useToken hook. The MDX doc blocks live in @unpunnyfuns/swatchbook-blocks, which this package depends on and re-exports; importing either package’s name gets you the blocks.

Terminal window
npm install -D @unpunnyfuns/swatchbook-addon @unpunnyfuns/swatchbook-core

This gives you the toolbar, preview decorator, and the useToken() hook. The addon depends on @unpunnyfuns/swatchbook-blocks and re-exports its full surface (TokenTable, ColorPalette, TokenDetail, TokenNavigator, Diagnostics, SwatchbookProvider, block-side hooks), so no separate install is needed inside a Storybook that already registers the addon; import from either package’s name for the same re-exported surface. Install @unpunnyfuns/swatchbook-blocks directly only for a non-Storybook consumer of the blocks. See the authoring guide for MDX composition patterns.

Peer requirements: Storybook 10.1+ on the Vite builder, React 18+.

Register the addon in main.ts#addons with your config (inline config or configPath; see the config reference for the trade-off):

.storybook/main.ts
import { defineMain } from '@storybook/react-vite/node';
export default defineMain({
framework: '@storybook/react-vite',
addons: [
{
name: '@unpunnyfuns/swatchbook-addon',
options: {
config: {
resolver: 'tokens/resolver.json',
cssVarPrefix: 'ds',
},
},
},
],
});

Then opt the preview into the addon’s annotations (CSF Next factory) in preview.ts:

.storybook/preview.ts
import { definePreview } from '@storybook/react-vite';
import swatchbookAddon from '@unpunnyfuns/swatchbook-addon';
export default definePreview({
addons: [swatchbookAddon()],
});

swatchbookAddon(options?) accepts a presenters option, a PresenterRegistry merged over the built-ins per $type. One registration reaches every built-in block in both story renders and provider-less MDX doc blocks:

export default definePreview({
addons: [swatchbookAddon({ presenters: { color: DualSurfaceSwatch } })],
});

This is distinct from the preset AddonOptions passed in main.ts (config / configPath / integrations). See Creating custom presenters for the presenter contract and Presenter registry for the full override precedence.

  • Preview decorator: reads the active axis tuple, writes one data-<prefix>-<axis>="<context>" attribute per axis on <html> + story wrapper, mounts the per-theme CSS, emits INIT_EVENT to the manager. The prefix follows cssVarPrefix (default swatch).
  • Toolbar tool: a single swatchbook IconButton. Clicking opens a popover containing preset pills and one dropdown per axis. Escape and outside-click close it.

For a browsable tree + diagnostics surface, compose <Diagnostics /> + <TokenNavigator /> + <TokenTable /> on an MDX page. See the authoring guide’s dashboard example. The Diagnostics reference catalogs every group and message that can appear in the Design Tokens panel.

KeyTypePurpose
swatchbookAxesRecord<string, string>Active tuple. Preferred input.

The toolbar writes swatchbookAxes. Blocks’ starting color format comes from Config.defaultColorFormat instead of a toolbar control; hex falls back to space-separated rgb() (flagged with a ⚠ warning marker) for colors outside sRGB gamut. See useColorFormat for the full precedence order.

export const DarkBrandA = meta.story({
parameters: {
swatchbook: {
axes: { mode: 'Dark', brand: 'Brand A' },
},
},
});

axes is a tuple; omitted keys fall back to axis defaults.

The addon exposes exactly one hook, under @unpunnyfuns/swatchbook-addon/hooks.

Typed lookup. Returns { value, cssVar, type?, description? } for the given path; the value is typed as unknown, type and description come from the token’s $type / $description if present. Reactive to axis changes.

import { useToken } from '@unpunnyfuns/swatchbook-addon/hooks';
function Brand() {
const accent = useToken('color.accent.bg');
return <span style={{ background: accent.cssVar }}>{accent.description}</span>;
}

Paths autocomplete from the generated .swatchbook/tokens.d.ts. Add "include": [".swatchbook/**/*.d.ts"] to your consumer tsconfig.

Reach for useToken, the doc blocks, or the Design Tokens panel rather than importing virtual:swatchbook/tokens directly: the virtual module is internal wiring and its shape isn’t a stable API. Prefer var(--…) in CSS where you can; useToken().cssVar returns the same reference string for programmatic use.

The block-side hooks (useSwatchbookData, useActiveTheme, useActiveAxes, useColorFormat) are re-exported from the addon’s main entry, so import { useActiveTheme } from '@unpunnyfuns/swatchbook-addon' works without a separate blocks dependency; they’re documented in the blocks hooks reference. The preview decorator mounts the SwatchbookProvider that backs them, so they resolve wherever the addon is registered.

import { ADDON_ID, AXES_GLOBAL_KEY, TOOL_ID, VIRTUAL_MODULE_ID } from '@unpunnyfuns/swatchbook-addon';

Useful when wiring the addon into custom tooling or writing interaction tests that read the globals directly.

Typed shape of the options object passed to the Storybook addons entry: what the preset reads at preview start.

import type { AddonOptions } from '@unpunnyfuns/swatchbook-addon';
interface AddonOptions {
/** Inline swatchbook config. Mutually exclusive with `configPath`. */
config?: Config;
/** Path to a config module, relative to the Storybook `configDir`. */
configPath?: string;
/**
* Display-side integrations plugged into the addon's Vite plugin.
* Each typically contributes a virtual module the preview imports
* (e.g. `virtual:swatchbook/tailwind.css`). The addon itself is
* tool-agnostic; integrations ship as separate packages.
*/
integrations?: SwatchbookIntegration[];
}

Config and SwatchbookIntegration come from @unpunnyfuns/swatchbook-core. See the Registration section above for a worked example, and the Integrations guide for integrations factories.