Skip to main content
Version: 0.61

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"). One dimension becomes two; two become four. Modelling each dimension independently keeps the combination-counting out of your theme names and into composition at runtime. 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

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.

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

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

  • 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

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

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