Consuming the active theme
swatchbook’s toolbar flips the active tuple, and a story rendering under it needs a way to notice the change. Three approaches, from least code to most:
- CSS variables: no code at all, and the right choice for any component that already reads tokens through
var(--…). - The React hook
useActiveAxes(): for story code inside the Storybook preview, reading the same React context the blocks and decorators use. - DOM observation: the framework-agnostic fallback for Vue / Angular / Svelte / plain HTML, or React trees outside the preview’s provider.
CSS variables (recommended)
Section titled “CSS variables (recommended)”swatchbook emits CSS custom properties keyed on the active tuple. A component reading those variables updates automatically when the toolbar flips an axis:
.my-button { background: var(--<prefix>-color-accent-bg); color: var(--<prefix>-color-accent-fg); border: 1px solid var(--<prefix>-color-border-default);}No JavaScript, no subscriptions, no observers. The cascade does the work. If your stories already use CSS variables for theming, you’re done; the rest of this page is for component libraries that read their theme as a JS object at render time.
For CSS-in-JS libraries whose theme values can be var(--…) strings (styled-components, emotion with a var(...)-based theme), see the CSS-in-JS integration; it publishes a ready-made theme accessor without either of the paths below.
React hook inside the preview
Section titled “React hook inside the preview”If the consumer is a React story / block / decorator running inside the Storybook preview, use the hooks the addon already ships:
import { useActiveAxes, useActiveTheme, useColorFormat } from '@unpunnyfuns/swatchbook-addon';
function ThemeBridge({ children }: { children: ReactNode }) { const axes = useActiveAxes(); // { mode: 'Dark', brand: 'Brand A', contrast: 'Normal' } const theme = mapAxesToMyTheme(axes); return <ThemeProvider theme={theme}>{children}</ThemeProvider>;}useActiveAxes() reads from the same React context the blocks package publishes. It’s populated by swatchbook’s preview decorator, so anything inside a story tree sees it. It’s reactive: components re-render when the toolbar flips an axis; no observer, no manual subscription.
useActiveTheme() gives the composed tuple string ("Dark · Brand A · Normal"); useColorFormat() gives the active color-format selection. Same shape, same context.
DOM observation (for everything else)
Section titled “DOM observation (for everything else)”The hooks above require React and the preview’s provider tree. When your consumer is neither (a Vue / Angular / Svelte app, plain HTML, or a React tree outside swatchbook’s provider), subscribe to the active tuple via MutationObserver on the DOM contract instead:
function readAxes(): Record<string, string> { const el = document.documentElement; const out: Record<string, string> = {}; for (const name of el.getAttributeNames()) { if (name.startsWith('data-<prefix>-')) { out[name.slice('data-<prefix>-'.length)] = el.getAttribute(name) ?? ''; } } return out;}
const observer = new MutationObserver(() => apply(readAxes()));observer.observe(document.documentElement, { attributes: true });apply(readAxes());Wrap that in whatever idiom your framework uses. Vue (onMounted + ref), Angular (a service with RxJS or a signal), Svelte (onMount + a writable store), plain HTML (attach to application state directly). The DOM contract is identical across frameworks.
For React outside the preview’s provider tree, the same pattern applies:
function useSwatchbookAxes(): Record<string, string> { const [axes, setAxes] = useState(readAxes); useEffect(() => { const observer = new MutationObserver(() => setAxes(readAxes())); observer.observe(document.documentElement, { attributes: true }); return () => observer.disconnect(); }, []); return axes;}Inside the preview, prefer useActiveAxes(): it updates without the observer cost and pools with the blocks’ own re-renders.
What the DOM contract guarantees
Section titled “What the DOM contract guarantees”data-<prefix>-<axisName>="<context>"attributes on<html>: one per active axis.<prefix>follows yourcssVarPrefixconfig (defaultswatch).- The same attributes on each story’s wrapper element. Redundant with
<html>in most cases, but useful if your story iframe hosts multiple wrappers. - Per-tuple CSS emitted under compound attribute selectors, e.g.
[data-<prefix>-mode="Dark"][data-<prefix>-brand="Brand A"] { … }. One block per tuple, with the default tuple’s values in:rootas the fallback. - Attributes update on toolbar flip. Synchronously, before the next paint; observers fire on the microtask queue.
Any change to this shape is a breaking change. The DOM is the API.
What swatchbook doesn’t ship
Section titled “What swatchbook doesn’t ship”- No Vue, Angular, or Svelte bindings. React is the only framework with shipped hooks (
useActiveAxes/useActiveTheme/useColorFormat), because swatchbook’s blocks are React and the addon’s preview decorator publishes a React context. Other frameworks consume the DOM contract directly: about ten lines of observer code in whatever idiom the framework uses. - No recipes per component library. MUI, Chakra, Mantine, styled-components, emotion, Tailwind class-mode: each has its own theming model. The CSS-variable and DOM-attribute contracts above are the starting points; your component library’s theming docs are the rest.
Known gotchas
Section titled “Known gotchas”Component library theme is a plain JS object: can it read the --<prefix>-* variables directly?
Only if the library consumes CSS variables natively. When the theme is a JS object read at render time (MUI’s createTheme, Chakra, Mantine), it can’t; rebuild the theme object on axis flips with useActiveAxes() in React, or the DOM-observation path elsewhere.
Tailwind’s dark: variant isn’t responding to swatchbook.
See the Tailwind integration for keying Tailwind’s dark variant on swatchbook’s attribute.
Storybook’s own manager theme (light/dark chrome around the preview) doesn’t flip.
That’s @storybook/theming, which controls Storybook’s UI chrome, a separate concern. See Storybook’s manager theming docs.
See also
Section titled “See also”- Axes reference: the runtime model for tuples and attribute composition.
- Integrations: zero-code paths for Tailwind and CSS-in-JS.