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).

Storybook addon injection pipeline Three-stage diagram showing addons registered in main.ts at build time, decorator chains in preview.ts at render time, and panel UIs in the manager at display time. BUILD TIME main.ts addons[ ] registry @storybook/addon-essentials @storybook/addon-a11y @chromatic-com/storybook @storybook/addon-interactions resolve RENDER TIME preview.ts decorator chain ThemeProvider decorator (wraps every Story) MSW decorator (network isolation) a11y / interactions (per-story hooks) render DISPLAY TIME Manager UI addon panels Controls panel (argtypes) Accessibility panel (axe) Interactions panel Chromatic diff viewer Each stage has a distinct responsibility — misconfigurations at stage 1 surface as build errors; at stage 2 as render failures; at stage 3 as missing panels

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-interactions panel surfaces play-function results inline, but the test-runner CI 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 maxDiffPixelRatio correctly 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.



^ Back to Storybook Isolation Workflows