Addon
Published as @unpunnyfuns/swatchbook-addon. Storybook addon. Loads your config at build time via @unpunnyfuns/swatchbook-core, exposes the resolved graph as a virtual module, registers a preview decorator + manager toolbar, and ships a useToken hook. Block-side hooks (useSwatchbookData, useActiveTheme, useActiveAxes, useColorFormat) live in @unpunnyfuns/swatchbook-blocks.
Install
npm install -D @unpunnyfuns/swatchbook-addon @unpunnyfuns/swatchbook-core
This gives you the toolbar, preview decorator, and the useToken() hook. The MDX doc blocks (TokenTable, ColorPalette, TokenDetail, TokenNavigator, Diagnostics, SwatchbookProvider, block-side hooks) live in @unpunnyfuns/swatchbook-blocks; install it alongside if you want them. See the authoring guide for MDX composition patterns.
Peer requirements: Storybook 10.3+ on the Vite builder, React 18+.
Registration
Register the addon in main.ts#addons with your config (inline config preferred; configPath also works, see the config reference):
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:
import { definePreview } from '@storybook/react-vite';
import swatchbookAddon from '@unpunnyfuns/swatchbook-addon';
export default definePreview({
addons: [swatchbookAddon()],
});
What it registers
- 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, emitsINIT_EVENTto the manager. The prefix followscssVarPrefix(defaultswatch). - Toolbar tool: a single Swatchbook
IconButton. Clicking opens a popover containing preset pills, one dropdown per axis, and a color-format picker (hex/rgb/hsl/oklch/raw). 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.
Globals
| Key | Type | Purpose |
|---|---|---|
swatchbookAxes | Record<string, string> | Active tuple. Preferred input. |
swatchbookColorFormat | 'hex' | 'rgb' | 'hsl' | 'oklch' | 'raw' | Display-only color format consumed by the blocks (TokenTable, ColorPalette, TokenDetail, TokenNavigator). Does not affect emitted CSS. Default hex. |
The toolbar writes swatchbookAxes. swatchbookColorFormat is toggled from the color-format picker in the popover; hex falls back to space-separated rgb() (flagged with a ⚠ warning marker) for colors outside sRGB gamut.
Per-story overrides
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 themeName: 'Composed Name' form sets the active tuple by composed theme name.
Hooks
The addon exposes exactly one hook, under @unpunnyfuns/swatchbook-addon/hooks.
useToken(path)
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.
Block-side hooks live in @unpunnyfuns/swatchbook-blocks
useSwatchbookData, useActiveTheme, useActiveAxes, useColorFormat, and the contexts that back them (SwatchbookContext, ThemeContext, AxesContext, ColorFormatContext) live canonically in @unpunnyfuns/swatchbook-blocks; see the blocks reference. They're also re-exported from the addon's main entry (import { useActiveTheme } from '@unpunnyfuns/swatchbook-addon' works) so consumers picking up the addon don't need a separate blocks dependency for the common hook surface.
The preview decorator mounts a SwatchbookProvider (from blocks) that populates every context, so the hooks resolve wherever the addon is registered.
Exported constants
import {
ADDON_ID,
AXES_GLOBAL_KEY,
COLOR_FORMAT_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.
AddonOptions
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.
Do / don't
- ✅ Use
useTokenfor typed lookups when you need the resolved value at runtime (aria labels, conditional rendering). - ✅ Prefer
var(--…)in CSS;useToken().cssVargives you the right string programmatically. - ❌ Don't import from
virtual:swatchbook/tokensdirectly in consumer code. Go throughuseToken/ the panel / the doc blocks so the API stays stable. - ❌ Don't combine
parameters.swatchbook.themeNameand the toolbar for the same story: the parameter wins and the toolbar change won't stick.