Storybook Addon Ecosystems
This page is part of the Storybook Isolation Workflows guide. Addons sit at the intersection of every tool in that workflow: they intercept the component rendering pipeline to inject accessibility audits, visual diffing engines, and interaction test runners—all within the same sandboxed environment that isolates stories from application-level state.
Getting addon configuration right is the foundation that everything else rests on. A misconfigured registry or unpinned dependency will produce irreproducible CI failures that look like component regressions when they are actually environment drift.
Prerequisites
How the addon pipeline fits into Storybook’s architecture
Before touching config, it helps to see where addons intercept the rendering cycle. The diagram below shows the three injection points: the main.ts registry (build time), the preview.ts decorator chain (render time), and the manager panel (UI time).
Step-by-step implementation
Step 1 — Register addons in main.ts with explicit version pins
Intent: Lock the addon registry at the project level so every developer and every CI runner resolves the same versions.
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: [
// Essentials bundles: controls, actions, viewport, backgrounds, toolbars
'@storybook/addon-essentials',
// Play-function-driven interaction tests (Testing Library under the hood)
'@storybook/addon-interactions',
// Axe-core accessibility panel — runs on every story render
'@storybook/addon-a11y',
// Chromatic visual regression upload + snapshot comparison
'@chromatic-com/storybook',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
core: {
// Prevent anonymous usage data from being sent during CI runs
disableTelemetry: true,
},
};
export default config;
Verify: npx storybook dev --no-open exits without “Failed to load preset” warnings. The browser console must show zero Cannot find module errors.
Pin versions in package.json overrides to prevent peer-dependency drift:
{
"overrides": {
"storybook": "8.3.6",
"@storybook/react": "8.3.6",
"@storybook/react-vite": "8.3.6",
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
}
Step 2 — Chain decorators in preview.ts for sandbox state injection
Intent: Ensure every story renders inside a controlled environment — theme provider, MSW network boundary, and viewport defaults — before any addon intercepts it. The order of decorators matters: outermost decorators run first.
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { ThemeProvider } from '../src/design-system/ThemeProvider';
// Start MSW service worker in development; no-op in CI static build
initialize({ onUnhandledRequest: 'warn' });
const preview: Preview = {
loaders: [mswLoader],
decorators: [
// 1. Outermost: theme context (all stories share the same token set)
(Story) => (
<ThemeProvider theme="light">
<Story />
</ThemeProvider>
),
// 2. Isolation wrapper — provides a stable DOM anchor for addon selectors
(Story) => (
<div className="storybook-sandbox" data-testid="addon-sandbox">
<Story />
</div>
),
],
parameters: {
// Default viewport for visual regression baselines
viewport: {
defaultViewport: 'desktop',
viewports: {
mobile: { name: 'Mobile 375', styles: { width: '375px', height: '812px' } },
tablet: { name: 'Tablet 768', styles: { width: '768px', height: '1024px' } },
desktop: { name: 'Desktop 1280', styles: { width: '1280px', height: '800px' } },
},
},
// Chromatic snapshot settings shared across all stories
chromatic: {
// Capture at all three viewports by default; override per story if needed
viewports: [375, 768, 1280],
// Disable animations to eliminate timing-driven visual drift
pauseAnimationAtEnd: true,
},
// a11y: treat 'color-contrast' as a warning in dev, error in CI
a11y: {
config: {
rules: [{ id: 'color-contrast', enabled: true }],
},
},
},
};
export default preview;
Verify: Open any story. The Accessibility panel must show a result (not a spinner). The Chromatic panel must show “This story will be captured”. If MSW is configured, the Network tab must show requests intercepted rather than hitting a real API.
Step 3 — Configure visual regression parameters per-story
Intent: Each component variant that represents a distinct visual regression baseline needs its own Chromatic snapshot configuration. Sharing a single global configuration causes missed regressions when only one viewport breaks.
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
parameters: {
// Disable Chromatic on this story group's default export;
// opt specific stories in below
chromatic: { disableSnapshot: true },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: 'primary', children: 'Submit order' },
parameters: {
chromatic: {
// This story IS a regression baseline — enable snapshot
disableSnapshot: false,
viewports: [375, 1280],
// Delay to let CSS transitions settle before capture
delay: 300,
},
},
};
export const Disabled: Story = {
args: { variant: 'primary', disabled: true, children: 'Submit order' },
parameters: {
chromatic: { disableSnapshot: false, viewports: [375, 1280] },
},
};
export const Loading: Story = {
args: { variant: 'primary', loading: true, children: 'Submitting…' },
parameters: {
chromatic: {
disableSnapshot: false,
// Pause CSS animation at end frame before capture
pauseAnimationAtEnd: true,
delay: 500,
},
},
};
Verify: Run npx chromatic --dry-run --project-token=<token> and confirm only the three stories above appear in the capture list. The --dry-run flag exits without uploading, making this safe in pull-request previews.
Step 4 — Wire Chromatic and Playwright into CI gating jobs
Intent: Block merges automatically when visual deltas exceed thresholds or interaction tests fail. The --exit-zero-on-changes flag lets Chromatic report diffs without failing the build — a reviewer must manually accept them in the Chromatic UI.
# .github/workflows/visual-regression.yml
name: Visual regression & interaction tests
on:
pull_request:
branches: [main, develop]
jobs:
storybook-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
# Build static Storybook for Playwright and Chromatic
- run: npx storybook build --output-dir storybook-static
env:
NODE_ENV: test
# Run play-function interaction tests via Storybook test runner
- run: npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue"
"npx http-server storybook-static --port 6006 --silent"
"npx wait-on tcp:6006 && npx storybook-test-runner --url http://localhost:6006 --ci"
# Upload snapshots to Chromatic; creates a review step for visual diffs
- name: Chromatic visual regression
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
exitOnceUploaded: true
onlyChanged: true # only re-capture stories whose files changed
Verify: After a pull request that changes a component’s styles, the “Chromatic” check must show “UI Review” status (not a green tick), which forces a visual sign-off before merge.
Step 5 — Audit and prune the addon registry
Intent: Accumulated addons slow cold-start times and increase the risk of version conflicts. Run a structured audit every quarter or whenever a major Storybook upgrade lands.
# Check for compatibility warnings and unmet peer deps
npx storybook doctor
# List installed addons and their resolved versions
npm ls --depth=0 | grep storybook
# Upgrade all Storybook packages together (respects the monorepo structure)
npx storybook upgrade
# After upgrade: run the full visual suite to catch regressions introduced by the upgrade
npm run test:storybook -- --ci
To automate upgrade scheduling without blindly automerging, use Renovate:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"packageRules": [
{
"matchPackagePatterns": ["^storybook$", "^@storybook/", "^@chromatic-com/"],
"groupName": "storybook-ecosystem",
"automerge": false,
"schedule": ["every weekend"],
"minimumReleaseAge": "7 days"
}
]
}
Verify: npx storybook doctor exits with no errors. npm run test:storybook -- --ci exits 0.
Configuration reference
| Option | Location | Type | Default | Effect |
|---|---|---|---|---|
addons |
main.ts |
string[] |
[] |
Registers addons at build time; order affects panel tab order |
core.disableTelemetry |
main.ts |
boolean |
false |
Suppresses anonymous usage pings — set true in all projects |
decorators |
preview.ts |
Decorator[] |
[] |
Global wrappers applied outermost-first around every story |
parameters.chromatic.viewports |
preview.ts / story |
number[] |
[1200] |
Pixel widths at which Chromatic captures snapshots |
parameters.chromatic.pauseAnimationAtEnd |
preview.ts / story |
boolean |
false |
Freezes CSS animations before capture — essential for loaders and skeletons |
parameters.chromatic.delay |
story | number (ms) |
0 |
Waits N ms after render before capture; use for transitions |
parameters.chromatic.disableSnapshot |
story | boolean |
false |
Opts a story out of visual capture entirely |
parameters.a11y.config.rules |
preview.ts / story |
Rule[] |
[] |
Per-rule axe configuration: enabled, disabled, or warn |
onlyChanged |
Chromatic CI action | boolean |
false |
Re-captures only stories in changed files — speeds up large repos |
exitZeroOnChanges |
Chromatic CI action | boolean |
false |
Prevents the CI job from failing on visual diffs; requires manual UI review |
Common pitfalls
1. Adding an addon to main.ts before installing the package.
The error surface is a cryptic webpack/Vite resolution failure at build start, not a clear “addon not found” message. Always run npm install <addon> before adding its string to the addons array.
2. Decorator order in preview.ts reversing context nesting.
Storybook applies decorators from the bottom of the array outward, so the last decorator in the array is the outermost wrapper. If your ThemeProvider must be the outermost wrapper, it must be the last entry — the opposite of what most developers expect. Test by inspecting the rendered DOM in the Storybook iframe.
3. Forgetting to pin peer dependencies after a major Storybook upgrade.
Storybook 8 requires react@18 and react-dom@18 as peer deps. Without overrides or resolutions, a monorepo can resolve two different React instances: one for the app, one for Storybook. This causes Invalid hook call errors that are extremely hard to trace back to the real cause.
4. Running @storybook/addon-interactions without @storybook/test-runner in CI.
The addon shows interaction results in the panel during development but does not fail the CI job by itself. To gate merges on interaction test failures, @storybook/test-runner (which executes play functions via Playwright headless) must be added to the CI job separately.
5. Enabling Chromatic snapshot capture on every story.
Stories that render dynamic data (dates, UUIDs, user avatars) will generate spurious diffs every run. Use chromatic: { disableSnapshot: true } at the story file level and opt specific stories in with disableSnapshot: false. Pair this discipline with component variants that use stable fixture data.
Integration points
The addon ecosystem connects to several adjacent workflows:
- Argtable mapping: Controls panel data is only as useful as the argtype definitions beneath it. When argtypes accurately mirror a component’s TypeScript interface, the Controls addon becomes a live prop editor that QA engineers can use to reproduce edge cases without modifying story files.
- Component variants: The set of stories that Chromatic captures is your visual regression baseline. Variants that are not represented as stories cannot be protected by snapshot comparison.
- Interaction testing: The
addon-interactionspanel surfaces play-function results inline, but thetest-runnerCI job is what enforces the gate. Configure both together. - Visual regression snapshot strategies: Chromatic is one of several diffing approaches. Understanding tolerance thresholds and pixel diff algorithms helps set
maxDiffPixelRatiocorrectly so the CI gate is tight without being flaky. - Mock boundaries: MSW in preview.ts creates the network isolation that makes snapshot baselines stable. A story that hits a real API endpoint will produce different pixels every time it renders.
FAQ
How many addons can Storybook load before build performance degrades?
There is no hard ceiling, but each addon that registers a panel or decorator contributes to the iframe bundle and the manager bundle separately. Beyond about eight active addons, cold-start times typically exceed 8 seconds on a 2-core CI runner. Profile with DEBUG='storybook:core' npx storybook dev to identify slow presets. Disable non-essential addons in CI using the disabledAddons array (available in @storybook/manager-api) or by conditionally including them in main.ts based on process.env.CI.
Can I use @storybook/addon-interactions and Playwright in the same pipeline?
Yes — they cover different scopes. addon-interactions runs play functions inside the Storybook iframe using Testing Library and is suited for unit-level interaction assertions (a button becomes disabled after click, a modal closes on Escape). Playwright drives a real browser and is suited for full visual snapshots, cross-browser compatibility checks, and end-to-end flows that span multiple story states. Configure both, but keep their failure surfaces separate so a play-function failure does not mask a visual diff.
What causes Cannot find module errors when registering a new addon?
The three most common causes are: (1) the addon package was not installed (npm install was not run after editing main.ts); (2) the import path uses the wrong scope (@storybook/addon-a11y vs @storybook-addons/a11y from an older version); (3) the addon’s own peer dependencies are not satisfied. Run npm ls @storybook/addon-a11y to verify the installed version and npx storybook doctor to surface peer dependency issues.
How do I prevent the a11y addon from blocking CI on known violations?
Suppress specific axe rules at the story level via parameters.a11y.config.rules. For example, to mark a known color-contrast issue as a warning rather than an error on a legacy component, add { id: 'color-contrast', enabled: false } to the story’s a11y.config.rules array and include a comment that links to the tracking issue. This avoids silencing the rule project-wide while keeping CI green during a phased remediation.
Related
- Essential Storybook Addons for Design System Maintainers — curated list of addons with install and config notes for design system teams
- Component Variants — defining the story set that forms your visual regression baseline
- Argtable Mapping — aligning Controls panel inputs with TypeScript prop interfaces
- Interaction Testing — running play functions as CI-gated tests with the Storybook test runner
- Visual Regression Snapshot Strategies — choosing between Chromatic, Playwright, and Percy; configuring thresholds