Skip to main content
Version: 0.13

Migrating from @storybook/addon-themes

Swatchbook's addon replaces @storybook/addon-themes outright — same toolbar role, richer model (multi-axis, DTCG-native, HMR-aware). If you're coming from addon-themes, this guide maps each of its decorators to the swatchbook equivalent.

TL;DR

addon-themesSwatchbook equivalent
withThemeByClassName({ themes, defaultTheme })Remove — swatchbook's toolbar sets data-<prefix>-* attributes by default; rewrite any class-based CSS to target [data-<prefix>-mode="Dark"] etc.
withThemeByDataAttribute({ themes, defaultTheme, attributeName })Remove — swatchbook does exactly this, multi-axis. Your stylesheet already consumes the attributes; keep it.
withThemeFromJSXProvider({ Provider, GlobalStyles, themes })Keep using your provider, but feed it swatchbook's theme accessor. See CSS-in-JS integration.

Your app's CSS stays the same. Your stories stay the same. You delete one decorator and either lose it (class / data-attr cases) or rewire it against the swatchbook provider (JSX case).

Step by step

1. Install the swatchbook addon

npm install -D @unpunnyfuns/swatchbook-addon

Register it in .storybook/main.ts alongside (or instead of) @storybook/addon-themes:

export default {
addons: [
- '@storybook/addon-themes',
+ '@unpunnyfuns/swatchbook-addon',
],
};

If you prefer a gradual migration, leave @storybook/addon-themes registered — both toolbars can coexist during the transition, though users will see two switchers.

2. Define your axes

addon-themes expects a flat themes: Record<string, string> map. Swatchbook expects a DTCG resolver or authored axes. The minimal swatchbook.config.ts for a single-axis setup:

// swatchbook.config.ts
import { defineSwatchbookConfig } from '@unpunnyfuns/swatchbook-core';

export default defineSwatchbookConfig({
tokens: ['tokens/base/**/*.json'],
axes: [
{
name: 'mode',
default: 'Light',
contexts: {
Light: [],
Dark: ['tokens/modes/dark.json'],
},
},
],
});

Multi-axis (mode × brand × contrast, for example) adds more axes entries — see the multi-axis walkthrough.

If your tokens are already in DTCG 2025.10 resolver format, point at the resolver file instead:

export default defineSwatchbookConfig({ resolver: 'tokens/resolver.json' });

3. Drop the addon-themes decorator

Were you using withThemeByClassName?

Your stylesheet probably looks like:

.dark { /* dark overrides */ }
.light { /* light overrides */ }

Option A: Rewire to data-* attrs. Swatchbook's toolbar sets data-<prefix>-mode="Dark" on <html> by default. Replace your class selectors:

-.dark { … }
+[data-sb-mode="Dark"] { … }

If you want the class form instead, keep swatchbook's data attributes as the primary mechanism and add a [data-sb-mode="Dark"] { }class bridge in your CSS:

[data-sb-mode="Dark"] { }
/* Synonym for anywhere you can't update `.dark` to the attribute selector: */
.dark { /* your overrides */ }

Option B: Emit CSS vars with projectCss. The cleaner migration — once your DTCG tokens are authored, swatchbook emits per-tuple CSS that redefines --<prefix>-* vars under [data-<prefix>-*] selectors. Your components reference the vars directly; class names stop mattering for theming. See the token pipeline.

Were you using withThemeByDataAttribute?

Your stylesheet already consumes data-* attributes. Swatchbook sets the same attributes (prefixed, by default — data-sb-mode rather than bare data-theme). Two choices:

Option A: Update your CSS to swatchbook's prefixed attrs.

-[data-theme="dark"] { … }
+[data-sb-mode="Dark"] { … }

Option B: Drop the prefix in swatchbook's config so the attribute matches your existing CSS.

defineSwatchbookConfig({
cssVarPrefix: '', // → `data-mode`, `data-brand`, etc., no prefix segment
// …
});

Beware: with an empty prefix, data-mode can collide with other libraries' conventions. Prefer Option A.

Were you using withThemeFromJSXProvider?

You have a real theme-object shape the provider expects (emotion's { colors: {…} }, styled-components' theme, MUI's createTheme output).

The migration depends on what the provider does with the theme.

  • If the provider just passes the theme through to useTheme() / props.theme and your styled components read values from it — emotion, styled-components, and hand-rolled providers — use the CSS-in-JS integration. It ships a virtual:swatchbook/theme module with var(--sb-*) string leaves; drop-in for any provider, toolbar flip handled by cascade.

  • If the provider derives dependent values from the theme at factory time — MUI's createTheme computing palette.primary.light / contrastText from a base — you need per-tuple resolved values, not var refs. Swatchbook doesn't ship that integration yet; the machinery exists in emitViaTerrazzo (resolver-backed projects, selection: 'presets', paired with @terrazzo/plugin-js), but you'd be wiring it yourself.

  • Vuetify theme config, bootstrap SCSS — same caveat as MUI. Same workaround.

4. Drop the global

addon-themes's theme global (string) doesn't survive the migration — swatchbook uses per-axis globals (swatchbookTheme, and optionally per-axis swatchbookAxes). Stories that read theme from globals need to switch:

-const { theme } = context.globals;
+const { swatchbookTheme } = context.globals;

Per-story overrides change from parameters: { themes: { themeOverride: 'dark' } } to parameters: { swatchbook: { themeOverride: 'Dark' } }. Context key follows the axis-aware swatchbook namespace.

5. Keep addon-themes around?

No. Swatchbook's toolbar supersedes it fully. Once your decorators are rewired, remove @storybook/addon-themes from addons[] and uninstall.

Why no bridge package?

It'd be tempting to ship a @unpunnyfuns/swatchbook-integrations/addon-themes subpath that auto-derives the themes / defaultTheme object withThemeByDataAttribute wants. We considered it and decided against: addon-themes' decorators are single-axis by design, and every bridge shape would either collapse multi-axis projects (losing expressiveness) or fight the addon's flat-string mental model. A documented migration path ends up cleaner than a bridge that half-works.

What you lose

  • themes: Record<string, string>'s flat mental model — swatchbook makes the multi-axis structure explicit, which is more accurate for most systems but requires one config decision (resolver vs layered axes).
  • withThemeFromJSXProvider's direct theme-object passing — our provider equivalent is via the CSS-in-JS integration, and MUI-style resolved-value factories aren't covered yet.

What you gain

  • Multi-axis (mode × brand × contrast × density × whatever) without flattening to 18 theme names.
  • DTCG 2025.10 as the source of truth — resolver.json instead of hand-wired theme maps.
  • HMR on token edits: change a color value, preview re-renders in place without losing args / scroll state.
  • Doc blocks (TokenTable, TokenNavigator, ColorPalette, TokenDetail) that read from the same source.
  • MCP server for AI-agent introspection over the same token graph.
  • Compound data-* selectors for runtime theme switching that compose naturally with Tailwind v4 and every CSS-var-consuming library.