Config reference
Every field on the Config object, with the semantics loadProject applies at runtime.
Two ways to supply it
Inline in .storybook/main.ts (recommended):
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',
default: { mode: 'Dark', brand: 'Brand A' },
cssVarPrefix: 'ds',
presets: [
{ name: 'A11y High Contrast', axes: { mode: 'Light', contrast: 'High' } },
],
},
},
},
],
});
Or in its own file, referenced by path:
import { defineSwatchbookConfig } from '@unpunnyfuns/swatchbook-core';
export default defineSwatchbookConfig({
resolver: 'tokens/resolver.json',
default: { mode: 'Dark', brand: 'Brand A' },
cssVarPrefix: 'ds',
});
{
name: '@unpunnyfuns/swatchbook-addon',
options: { configPath: '../swatchbook.config.ts' },
}
The separate file path (configPath) is useful when the same config is consumed by other tooling — a CLI, a CI lint job, a custom build script. For Storybook-only projects, inline is one less file to track. defineSwatchbookConfig is an identity helper — it gives you typed autocomplete on the object you pass in and returns it unchanged.
Picking a theming input
Exactly three shapes are legal:
- Resolver —
resolver: 'path/to/resolver.json'. The DTCG-spec-native path; the file declares sets, modifiers, and resolutionOrder, and the modifiers become your axes. Recommended. - Layered —
axes: [...]+tokens: [...]. For projects without a resolver document. You declare axes inline; each context names overlay files that layer onto the basetokens. - Plain parse —
tokens: [...]alone. Single syntheticthemeaxis; every token loaded as one tuple. Useful for smoke-testing a token set before adopting a resolver.
Setting resolver and axes at the same time is an error. Setting none of the three (no resolver, no axes, no tokens) throws at load time.
Fields
tokens?: string[]
Glob patterns (relative to the config's cwd) for base DTCG token files.
- Resolver projects (
resolverset) — optional. The resolver's own$reftargets fully determine which files get loaded, and the addon's Vite plugin derives HMR watch paths from the resolved source list. Supplyingtokensalongsideresolveris still valid and unions the watch paths — useful when you want HMR to pick up files the resolver doesn't reference directly. - Layered projects (
axesset) — required. The layered loader parses[...base, ...overlayFilesInAxisOrder]per tuple; withouttokensthere's nothing to layer onto. - Plain-parse (no resolver, no axes) — required. Every file matched by the globs gets parsed into the single synthetic theme.
resolver?: string
Path to a DTCG 2025.10 resolver document. Mutually exclusive with axes.
Terrazzo normalizes the document, then Swatchbook enumerates permutations via resolver.listPermutations() and realizes each with resolver.apply(). One Axis surfaces per modifier; the resolver file itself and every $ref target it pulls in land on Project.sourceFiles.
axes?: AxisConfig[]
Authored layered axes, for projects without a resolver. Mutually exclusive with resolver. Each AxisConfig has:
interface AxisConfig {
name: string;
description?: string;
contexts: Record<string, string[]>;
default: string;
}
contexts is a map from context name to an ordered list of overlay files (paths or globs, relative to the config's cwd). An empty array is legal — it means "no override," which is the baseline for an axis that introduces nothing by default (Default: []).
Themes are every combination of contexts across all axes. Last write wins on duplicate token paths across base + overlays; overlay files listed later in axes win over earlier ones.
default?: Partial<Record<string, string>>
Initial active tuple, keyed by axis name. Partial is fine — any axis you omit falls back to that axis's own default at load time. Unknown axis keys and invalid context values produce warn diagnostics (group swatchbook/default) and are sanitized out.
// Every axis specified:
default: { mode: 'Dark', brand: 'Brand A' }
// Partial — omitted axes use their own defaults:
default: { brand: 'Brand A' }
// Omit entirely — starts in the all-axis-defaults tuple:
// default: undefined
cssVarPrefix?: string
Prefix prepended to every emitted CSS variable. color.accent.bg becomes var(--<prefix>-color-accent-bg); an empty string disables the prefix. Typical values are short and project-specific (ds, sb, ui).
outDir?: string
Project-local directory for codegen artifacts (default .swatchbook). The addon preset writes tokens.d.ts here — add "include": [".swatchbook/**/*.d.ts"] to your tsconfig so useToken() autocompletes.
presets?: Preset[]
Named tuple combinations rendered as quick-select pills next to the toolbar's axis dropdowns. Each Preset has:
interface Preset {
name: string;
axes: Partial<Record<string, string>>;
description?: string;
}
Partial tuples are legal — omitted axes resolve to the axis's default when the preset is applied. loadProject validates presets: unknown axis keys and invalid context values surface as warn diagnostics (group swatchbook/presets) and are sanitized out, but the preset itself stays in Project.presets (an empty preset is still a valid tuple — it resolves to all defaults).
See Presets for the runtime UX.
disabledAxes?: string[]
Axis names to suppress from this Storybook session. Each listed axis is pinned to its default context: the toolbar dropdown disappears, the axis drops out of Project.axes, non-default tuples fold away, and CSS emission stops including the axis in compound selectors. The Design Tokens panel still shows a small pinned indicator so authors don't forget the modifier is being suppressed.
defineSwatchbookConfig({
resolver: 'tokens/resolver.json',
disabledAxes: ['contrast'],
cssVarPrefix: 'ds',
});
Config-level only — there's no runtime toggle. Disabling an axis is the cleanest way to hide a work-in-progress modifier (or an experimental axis you don't want surfaced yet) without editing the resolver document. Unknown axis names produce warn diagnostics (group swatchbook/disabled-axes) and are ignored; the filtered list lands on Project.disabledAxes for downstream tooling.
chrome?: Record<string, string>
Override one or more of swatchbook's ten block-chrome roles. Block components (<TokenTable>, <ColorPalette>, etc.) read a fixed --swatchbook-* CSS variable namespace for their surfaces, text, and accents — independent of cssVarPrefix, so your token namespace never collides with chrome reads. Every role is always declared: by default, to the hard-coded literals in DEFAULT_CHROME_MAP (which use light-dark() so chrome auto-flips with the active color-scheme — Storybook's light/dark preference, the OS, whatever). Any role you set in chrome replaces that literal with a var(--<prefix>-<your-token>) reference so chrome reflows with your own theme switches instead.
The ten roles (closed set — also exported as CHROME_ROLES and the ChromeRole type from @unpunnyfuns/swatchbook-core):
| Role | Reads as |
|---|---|
surfaceDefault | --swatchbook-surface-default |
surfaceMuted | --swatchbook-surface-muted |
surfaceRaised | --swatchbook-surface-raised |
textDefault | --swatchbook-text-default |
textMuted | --swatchbook-text-muted |
borderDefault | --swatchbook-border-default |
accentBg | --swatchbook-accent-bg |
accentFg | --swatchbook-accent-fg |
bodyFontFamily | --swatchbook-body-font-family |
bodyFontSize | --swatchbook-body-font-size |
Each entry emits a :root alias that redirects the chrome read to your target token's already-emitted CSS variable:
defineSwatchbookConfig({
resolver: 'tokens/resolver.json',
cssVarPrefix: 'ds',
chrome: {
surfaceDefault: 'color.brand.bg.primary',
textDefault: 'color.brand.fg.primary',
accentBg: 'color.brand.accent.primary',
// …
},
});
Produces a single :root chrome block with all ten roles declared — overrides become var(...) references that inherit per-theme values automatically, the rest stay on DEFAULT_CHROME_MAP's hard-coded literals:
:root {
color-scheme: light dark;
--swatchbook-surface-default: var(--ds-color-brand-bg-primary);
--swatchbook-surface-muted: light-dark(#f4f4f5, #1e293b);
--swatchbook-surface-raised: light-dark(#ffffff, #111827);
--swatchbook-text-default: var(--ds-color-brand-fg-primary);
--swatchbook-text-muted: light-dark(#4b5563, #94a3b8);
--swatchbook-border-default: light-dark(#e5e7eb, #334155);
--swatchbook-accent-bg: var(--ds-color-brand-accent-primary);
--swatchbook-accent-fg: light-dark(#ffffff, #0b1220);
--swatchbook-body-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--swatchbook-body-font-size: 14px;
}
Composite sub-field targets are accepted. typography.body is a composite token that emits one sub-variable per field; the chrome map can target those directly (bodyFontSize: 'typography.body.font-size') even though the sub-field isn't a standalone token ID.
Unknown role keys (outside the ten above) and target paths that don't resolve to any token or composite sub-field in any theme produce warn diagnostics (group swatchbook/chrome) and are dropped — the corresponding chrome slot falls back to its hard-coded literal default. The validated entries land on Project.chrome for downstream tooling.
See also
- Theming inputs — when to pick resolver vs layered.
- Axes — the runtime model for selection.
- Reference: core — the loader APIs this config drives.