Config reference
Every field on the Config object, with the semantics loadProject applies at runtime.
Two ways to supply it
Section titled “Two ways to supply it”Inline in .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', 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, the inline form keeps everything in one file. defineSwatchbookConfig wraps the config for typed autocomplete and returns it unchanged.
Picking a theming input
Section titled “Picking a theming input”Config has three valid shapes: ResolverConfig | LayeredConfig | PlainConfig. Which load-strategy field is present determines the shape, and invalid combinations (like { resolver, axes } or { axes } without tokens) are compile-time errors. The runtime also validates the same constraints for plain-JS callers.
Exactly three shapes are legal:
- Resolver (
ResolverConfig):resolver: 'path/to/resolver.json'. The DTCG-spec-native path; the file declares sets, modifiers, and resolutionOrder, and the modifiers become your axes. - Layered (
LayeredConfig):axes: [...]+tokens: [...]. For projects without a resolver document. You declare axes inline; each context names overlay files that layer onto the basetokens. - Plain parse (
PlainConfig):tokens: [...]alone. Single syntheticthemeaxis; every token loaded as one tuple. Useful for smoke-testing a token set before adopting a resolver.
All three variants extend a shared base (cssVarPrefix, presets, disabledAxes, chrome, indicators, cssOptions, listingOptions, terrazzoPlugins, maxJointArity, default, outDir).
Fields
Section titled “Fields”tokens
Section titled “tokens”Type: string[], optional.
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
Section titled “resolver”Type: string, optional.
Path to a DTCG 2025.10 resolver document. Mutually exclusive with axes.
Terrazzo normalizes the document, then swatchbook enumerates individual tuples (one per non-default (axis, context)) and realizes each via resolver.apply(). One Axis surfaces per modifier; the resolver file itself and every $ref target it pulls in land on Project.sourceFiles.
Type: AxisConfig[], optional.
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: []).
The loader materializes one entry per non-default context per axis, never every combination at once. Last write wins on duplicate token paths across base + overlays; overlay files listed later in axes win over earlier ones.
default
Section titled “default”Type: Partial<Record<string, string>>, optional.
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: undefinedcssVarPrefix
Section titled “cssVarPrefix”Type: string, optional. Default swatch.
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
Section titled “outDir”Type: string, optional. Default .swatchbook.
Project-local directory for codegen artifacts. The addon preset writes tokens.d.ts here; add "include": [".swatchbook/**/*.d.ts"] to your tsconfig so useToken() autocompletes.
presets
Section titled “presets”Type: Preset[], optional.
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, resolving to all defaults).
disabledAxes
Section titled “disabledAxes”Type: string[], optional.
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
Section titled “chrome”Type: Record<string, string>, optional.
Override one or more of swatchbook’s nine 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 each resolves to a hard-coded literal in DEFAULT_CHROME_MAP (a flat hex string or font stack for a standalone light-scheme block surface). 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 nine 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 |
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 nine roles declared: overrides become var(...) references that inherit per-theme values automatically, the rest stay on DEFAULT_CHROME_MAP’s hard-coded literals:
:root { --swatchbook-surface-default: var(--ds-color-brand-bg-primary); --swatchbook-surface-muted: #f8fafc; --swatchbook-surface-raised: #ffffff; --swatchbook-text-default: var(--ds-color-brand-fg-primary); --swatchbook-text-muted: #475569; --swatchbook-border-default: #e2e8f0; --swatchbook-accent-bg: var(--ds-color-brand-accent-primary); --swatchbook-accent-fg: #ffffff; --swatchbook-body-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;}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 (bodyFontFamily: 'typography.body.font-family') even though the sub-field isn’t a standalone token ID.
Unknown role keys (outside the nine 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.
indicators
Section titled “indicators”Type: Record<string, boolean>, optional.
Project-wide baseline for the row-indicator strip on <TokenNavigator> and <TokenTable> (the per-row alias / variance / gamut / deprecation / description glyphs). Each key toggles one indicator: alias, variance, gamut, deprecation, description, composes. Unlike chrome, which travels as CSS variables, this baseline reaches blocks as runtime data over the same path cssVarPrefix follows.
defineSwatchbookConfig({ resolver: 'tokens/resolver.json', indicators: { description: true, },});Precedence runs lowest to highest: the hard-coded indicator defaults (alias, variance, gamut, deprecation on; description and composes off), then this config.indicators baseline, then a block’s own indicators prop. A block that sets indicators={{ description: false }} overrides the baseline above for that block; a block with no prop inherits the baseline.
defaultColorFormat
Section titled “defaultColorFormat”Type: 'hex' | 'rgb' | 'hsl' | 'oklch' | 'raw', optional, defaults to 'hex'.
Starting color format for blocks that display color values (TokenTable, ColorPalette, ColorTable, TokenDetail, TokenNavigator). Emitted CSS is unaffected: this is a display-only setting for the Storybook preview / docs blocks.
defineSwatchbookConfig({ resolver: 'tokens/resolver.json', defaultColorFormat: 'oklch',});A block’s own colorFormat prop overrides this, and so does wrapping a subtree in ColorFormatContext; see useColorFormat for the full precedence order.
cssOptions
Section titled “cssOptions”Type: Omit<CSSPluginOptions, 'variableName' | 'permutations' | 'filename' | 'skipBuild'>, optional.
Options forwarded to the @terrazzo/plugin-css instance swatchbook runs internally: for the stylesheet it emits for the Storybook preview and for the Token Listing’s names.css derivation. Use this when your production Terrazzo build passes non-default options to plugin-css (legacyHex, a custom transform, include / exclude globs, etc.) and you want the docs-side output to match.
Four fields are managed internally and stripped from the allowed type:
variableName: swatchbook routes naming through@terrazzo/token-tools/css.makeCSSVarwith yourcssVarPrefix; overriding this would desync the listing’snames.cssfrom the emitted stylesheet.permutations: synthesized one-per-theme so CSS emission is axis-aware; overriding breaks the preview’s toolbar cascade.filename: the internal stylesheet is captured in memory, never written to disk.skipBuild: settingtruewould null out the listing’spreviewValuederivation, breaking every block that displays a value.
Everything else passes through. Three plugin-css selectors (baseSelector, baseScheme, modeSelectors) pass the type check but swatchbook ignores them; permutations-based emission supersedes them. Setting any produces a swatchbook/css-options warn diagnostic.
A few pass-through options are worth calling out:
legacyHexandtransform, along with the other value-formatting options, reach both the emitted stylesheet andlisting[path].previewValue, because@terrazzo/plugin-token-listingderives preview values from plugin-css output. FlippinglegacyHex: truechanges the swatch CSS and every block that displays a value at once.include/excludefilter which tokens reach the emitted stylesheet, and nothing more: swatchbook still parses every token, so the listing enumerates them and the blocks display them. A story that reads a filtered-out token through a CSS var gets an unset variable.utilityopts into Terrazzo’s utility-class emission (.bg-color-accent-bg). Blocks don’t read utility output; it exists so the preview matches a production bundle that ships utility classes.
defineSwatchbookConfig({ resolver: 'tokens/resolver.json', cssVarPrefix: 'ds', cssOptions: { legacyHex: true, include: ['color.*', 'space.*'] },});See Aligning with your token build for the end-to-end shared-options pattern.
listingOptions
Section titled “listingOptions”Type: Omit<TokenListingPluginOptions, 'filename'>, optional.
Options forwarded to @terrazzo/plugin-token-listing. The listing is how swatchbook surfaces authoritative per-token metadata (plugin-css-computed CSS variable names, preview values, alias chains, source file + line references) into Project.listing and the block-side ProjectSnapshot.listing. Use listingOptions.platforms to register additional platforms beyond the built-in css entry:
import swiftPlugin from '@terrazzo/plugin-swift';
defineSwatchbookConfig({ resolver: 'tokens/resolver.json', cssVarPrefix: 'ds', listingOptions: { platforms: { css: { name: '@terrazzo/plugin-css' }, swift: { name: '@terrazzo/plugin-swift', description: 'UIColor' }, }, }, terrazzoPlugins: [swiftPlugin({ /* swift plugin options */ })],});Each platforms entry names a Terrazzo plugin whose identifier-naming logic derives the token’s identifier on that platform. The plugin has to be loaded in the build for the reference to resolve; see terrazzoPlugins below.
Two optional hooks refine what the listing records:
previewValue(token)overrides the auto-computed preview string per token; returnundefinedto fall through to the plugin-css value. Useful when production formats values for a non-CSS target you want the docs to mirror.subtype(token)classifies a token’s intent (bgColor,fgColor,borderColor,padding, …). Reserved for future per-subtypeblock defaults; blocks don’t read it yet, but setting it puts the metadata in place.
modes declares the resolver modes the listing enumerates. swatchbook’s own resolver reading already covers this for resolver projects; set it only for resolver-free layered projects that need modes enumerated explicitly.
filename is managed internally (the listing is captured in memory, not written to disk) and is stripped from the option type.
terrazzoPlugins
Section titled “terrazzoPlugins”Type: readonly Plugin[], optional.
Additional Terrazzo plugins loaded alongside swatchbook’s internal plugin-css + plugin-token-listing. Required when listingOptions.platforms references anything beyond the built-in css: the referenced plugin has to actually run in the same build for the listing to resolve its naming. Plugins whose output swatchbook doesn’t consume are harmless; they run, their output files land in the in-memory output set, nothing else happens.
Common use: load @terrazzo/plugin-swift / -android / -js / -sass alongside so their naming logic populates listing[path].names.<platform> for block consumption.
See Aligning with your token build for the pattern of sharing one options file between terrazzo.config.ts and swatchbook.config.ts.
maxJointArity
Section titled “maxJointArity”Type: number, optional. Default 4.
Maximum arity of joint-divergence detection per token. A joint divergence is a multi-axis tuple where the cartesian-correct value differs from what CSS cascade composes from the lower-arity blocks; each one emits a compound selector ([data-axis-a="…"][data-axis-b="…"]…) at emission time. The default of 4 covers the largest joint shapes real-world design systems tend to express (mode × brand × density × contrast or similar).
Bump to 5+ only when tokens genuinely diverge across five or more axes at once: without it, those tuples resolve to the cascade-composed value rather than the cartesian-correct one, showing as a wrong value at that specific combination. Lower it when load-time work is the concern. Per-token probe work grows with the number of axis combinations checked, Σ_{k=2..arity} C(|affectedBy|, k) × Π non-default contexts in combo, so tokens affected by many dense (large-context-count) axes dominate. maxJointArity: 1 caps below the arity-2 floor and emits no joint blocks at all.
See also
Section titled “See also”- Axes reference: the runtime model for selection.
- Reference: core: the loader APIs this config drives.
- Aligning with your token build: keep the swatchbook build aligned with a production Terrazzo CLI.