Axes
An axis is one independent dimension of your theming model: mode, brand, density, contrast, whatever fits your system. Each axis has a set of named contexts (one of which is the default), and at any moment exactly one context is selected per axis.
The axis shape is the alternative to a flat string-ID model ("light", "dark", "brand-a-dark-compact"). Flat IDs have to name every combination, and the count multiplies: with two contexts each, one dimension is two names, two dimensions four. Modelling each dimension independently keeps that counting out of theme names; contexts compose at runtime instead. If your system has exactly one dimension and no plans for more, @storybook/addon-themes’ single-string-ID model is simpler and a better fit.
Tuples, not strings
Section titled “Tuples, not strings”The canonical representation of “what theme is active” is a tuple: one context selected per axis. For a resolver with mode and brand:
{ mode: 'Dark', brand: 'Brand A' }Every resolved token, every CSS selector, every panel readout keys on this tuple. The composed string ("Dark · Brand A") exists for display.
A flip is a selection, not a rebuild: every value it applies was computed when the project loaded.
Data attributes
Section titled “Data attributes”When a tuple is active, the preview decorator writes one data-<prefix>-<axisName>="<context>" attribute per axis to <html> and the story wrapper. The prefix follows cssVarPrefix (default swatch):
<html data-swatch-mode="Dark" data-swatch-brand="Brand A">CSS emission matches those attributes with compound selectors. Each non-default tuple gets its own flat block (every var redeclared) with :root carrying the default:
:root { --swatch-color-surface-default: #ffffff;}[data-swatch-mode="Dark"][data-swatch-brand="Default"] { --swatch-color-surface-default: #0f172a;}[data-swatch-mode="Dark"][data-swatch-brand="Brand A"] { --swatch-color-surface-default: #111827;}Flat emission avoids collision-detection: DTCG allows multiple modifiers to write to the same token path, and flat blocks let the last-written tuple win cleanly.
These attributes and CSS vars are swatchbook’s preview scaffolding, not a public styling API. Consumer components shouldn’t key production theming off data-swatch-* selectors.
Independence
Section titled “Independence”Axes are independent. Selecting mode=Dark does not constrain brand; the resolver evaluates each modifier in resolutionOrder and each contributes its own layer stack. The toolbar popover reflects this by rendering one dropdown per axis, not a single flat dropdown listing every theme.
Where axes come from
Section titled “Where axes come from”- Resolver input (
config.resolver): each DTCG modifier becomes one axis (source: 'resolver'). - Layered config (
config.axes): each entry becomes one axis (source: 'layered'). - Neither input: a single synthetic axis named
theme(source: 'synthetic').
Read the active tuple from story code:
import { useActiveAxes } from '@unpunnyfuns/swatchbook-addon';
function MyComponent() { const axes = useActiveAxes(); // axes === { mode: 'Dark', brand: 'Brand A' }}Per-story overrides
Section titled “Per-story overrides”Force a specific tuple on a single story:
export const DarkBrandA = meta.story({ parameters: { swatchbook: { axes: { mode: 'Dark', brand: 'Brand A' }, }, },});Omit an axis and it falls back to that axis’s default. Invalid axis keys or context values are silently ignored (the decorator falls back to defaults).
Disabling an axis
Section titled “Disabling an axis”A resolver may declare more modifiers than a given Storybook wants to surface. List the axis name in config.disabledAxes and the project behaves as if that axis didn’t exist for this session: no toolbar dropdown, no extra CSS blocks, the axis pinned to its default context in the active tuple. The Design Tokens panel shows a faint pinned indicator so the suppression isn’t invisible. See the disabledAxes field for details.
See also
Section titled “See also”- Config reference: the
resolver/axes/default/disabledAxes/presetsfields. - Consuming the active theme: how stories pick up axis flips.