Skip to main content
Version: 0.61

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):

.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()],
});

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, 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, 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

KeyTypePurpose
swatchbookAxesRecord<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 useToken for typed lookups when you need the resolved value at runtime (aria labels, conditional rendering).
  • ✅ Prefer var(--…) in CSS; useToken().cssVar gives you the right string programmatically.
  • ❌ Don't import from virtual:swatchbook/tokens directly in consumer code. Go through useToken / the panel / the doc blocks so the API stays stable.
  • ❌ Don't combine parameters.swatchbook.themeName and the toolbar for the same story — the parameter wins and the toolbar change won't stick.