Integrations

Storybook

Run Frontguard against a Storybook to catch component-level visual regressions — without paying for Chromatic.

Storybook integration

Frontguard ships a first-class Storybook discovery adapter. Point it at a running Storybook (locally or in CI) and Frontguard will enumerate every story, navigate the preview iframe directly, wait for each story's play() function to finish, and capture multi-viewport screenshots — diffed against git-tracked baselines and (optionally) analysed by AI.

It is not a thin wrapper around the Playwright plugin: discovery comes from Storybook's own index.json, per-story metadata flows in through parameters.frontguard, and the renderer is play()-aware so the post-interaction state of a story is what ends up in your baseline.

Looking for the broader story (CLI flags, AI analysis, sandbox fix verification)? See Quick Start and the CLI reference — everything that works for full pages works the same way against Storybook stories.

Why use Frontguard with Storybook?

You probably already have a visual testing story (if you have one at all) that looks like one of these:

OptionStrengthsWhat's missing
ChromaticBuilt by the Storybook team, mature review UI, deep ecosystem integration.$179/mo+ above the 5k snapshot free tier, no production monitoring, no fix suggestions, locked to Storybook-only — full-page regressions never get tested.
Storybook test-runner + PlaywrightFree, runs in CI.You build your own diffing, no AI analysis, no baseline storage, no PR comments.
Lost Pixel / BackstopJSFree, decent diff UX.Pixel comparison only, no AI, no auto-fix, project is going stale (see BackstopJS migration guide).
Roll-your-own page screenshotsTotal control.You'll spend a quarter doing this and another quarter chasing flakes.

Frontguard's Storybook mode keeps the parts you actually liked about Chromatic — every story captured automatically, per-story configuration via parameters, CI-friendly — and adds the pieces it was missing:

  • AI classification — every diff is tagged regression, intentional, or content_update so you stop blind-approving everything.
  • Auto-fix suggestions — when the AI thinks it knows the CSS that broke, it ships you a patch. With verifyFixes: true it runs the patch in a local (or Daytona) sandbox and confirms the diff actually goes away.
  • Self-hosted by default — baselines live in a git orphan branch. Frontguard never charges per snapshot.
  • Same tool for stories AND full pages — flip storybook on for the component library, off for the marketing site. Same baselines layout, same PR comments, same dashboard.

Quick start

1. Install

npm install -D @frontguard/cli
npx playwright install chromium
pnpm add -D @frontguard/cli
pnpm exec playwright install chromium
yarn add -D @frontguard/cli
yarn playwright install chromium

2. Scaffold a Storybook-aware config

npx -p @frontguard/cli frontguard init

When frontguard init sees a .storybook/main.ts (or .js / .mjs / .cjs) in your project, it emits a Storybook-aware config and pre-points baseUrl at the Storybook dev server:

frontguard.config.ts
import type { FrontguardConfig } from '@frontguard/cli';

export default {
  version: 1,
  baseUrl: 'http://localhost:6006',
  viewports: [375, 768, 1440],
  browsers: ['chromium'],
  threshold: 0.1,
  ignore: [],
  smartRender: true,
  workers: 4,
  pageTimeout: 30_000,
  maxHeight: 5_000,
  outputDir: './frontguard-report',
  storybook: {
    url: 'http://localhost:6006',
    stories: ['**'], // enumerate every story; narrow with globs if needed
  },
} satisfies FrontguardConfig;

Force the storybook block on a non-Storybook project (rare) with frontguard init --storybook. Suppress detection with frontguard init --no-storybook.

3. Start Storybook + run Frontguard

# In one terminal — Storybook 8 listens on 6006 by default
npm run storybook

# In another terminal
npx -p @frontguard/cli frontguard run

The first run captures baselines; subsequent runs diff against them.

You should see something like:

✔ Storybook discovery (v8): found 23 stories
✔ Rendering — Chromium 1440px [23/23]
✔ All 23 stories match baselines

How discovery works

When storybook.url is set, Frontguard:

  1. Fetches the story index. It hits <url>/index.json (Storybook 8 and recent 7.6+) first. If that 404s, it falls back to <url>/stories.json (Storybook 7.x). Frontguard reports the detected major version in the discovery log.
  2. Filters out non-story entries. Anything with type === 'docs' is skipped — autodocs entries aren't capturable as a single preview frame.
  3. Applies your include / exclude filters (storybook.stories and storybook.exclude).
  4. Reads per-story parameters.frontguard to override viewports, threshold, ignore rules, or to skip stories entirely.
  5. Emits one route per story with the path /iframe.html?id=<story-id>&viewMode=story and discoveredVia: 'storybook'. The renderer uses that marker to wait for play() before capturing.

You don't need to write any Frontguard glue per story. Discovery is purely declarative — the source of truth is your stories themselves.

Filtering stories

storybook.stories and storybook.exclude both accept the same patterns:

  • A story id: 'components-button--primary'
  • A glob over title or title/name: 'Forms/*', 'Components/Button/*'
  • A title prefix: 'Components/Button'
frontguard.config.ts
export default {
  // ...
  storybook: {
    url: 'http://localhost:6006',
    // Only test buttons + form components
    stories: ['Components/Button/*', 'Forms/*'],
    // Skip work-in-progress stories
    exclude: ['*--wip', 'Internal/*'],
  },
} satisfies FrontguardConfig;

Per-story configuration

Story files own per-story configuration via parameters.frontguard. The shape is:

interface StoryFrontguardParameters {
  viewports?: number[];        // narrow / widen the viewport set
  threshold?: number;          // fraction 0–1, e.g. 0.005
  ignore?: IgnoreRule[];       // mask selectors / rects
  skip?: boolean;              // skip this story entirely
  label?: string;              // override the report label
}

Real-world examples:

src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta = { title: 'Components/Button', component: Button } satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: { variant: 'primary' },
  // No overrides — uses global config (375 / 768 / 1440, threshold 0.1).
};

export const Secondary: Story = {
  args: { variant: 'secondary' },
  parameters: {
    frontguard: {
      // This button looks identical at every width — only test once.
      viewports: [768],
      // Tighten the threshold because we know this surface is static.
      threshold: 0.005,
    },
  },
};

export const Danger: Story = {
  args: { variant: 'danger' },
  parameters: {
    frontguard: {
      // The hover ring's subtle gradient over-flags pixelmatch.
      ignore: [{ selector: '.fg-hover-ring' }],
    },
  },
};

export const InProgress: Story = {
  parameters: {
    // Don't test until I finish redesigning this.
    frontguard: { skip: true },
  },
};

Global defaults live in .storybook/preview.ts:

.storybook/preview.ts
import type { Preview } from '@storybook/react';

const preview: Preview = {
  parameters: {
    frontguard: {
      // Storybook-wide default viewports.
      viewports: [375, 768, 1440],
    },
  },
};

export default preview;

Frontguard reads parameters.frontguard from the story level when it exists. The preview-level block is informational today — set Storybook-wide defaults via frontguard.config.ts (viewports, threshold, ignore) for now, and override per story with the parameters.frontguard block.

play() awareness

Stories that ship a play() function are first-class. Frontguard's renderer detects a Storybook route, then injects a wait script that:

  1. Polls window.__STORYBOOK_PREVIEW__.storyRenders for a phase of completed / played / errored (Storybook 8).
  2. Subscribes to the preview 'storyRendered' channel event (Storybook 7).
  3. Falls back to a DOM-marker heuristic (body.sb-show-main AND not sb-show-preparingStory) when neither API is present.
  4. Times out after the global pageTimeout and captures anyway, with a warning in the log so you can debug a stuck play function.

That means stories like this one capture after the interaction:

src/components/Modal.stories.tsx
import { userEvent, within, expect } from '@storybook/test';
import { Modal } from './Modal';

export const OpenedByPlay = {
  args: { defaultOpen: false, triggerLabel: 'Open the modal' },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const trigger = await canvas.findByTestId('modal-trigger');
    await userEvent.click(trigger);
    await expect(await canvas.findByTestId('modal-overlay')).toBeInTheDocument();
  },
};

What you get in your baseline is the opened modal, not the closed trigger. If your previous Chromatic setup leaned on play() to set the state of an interactive component, your stories port over unchanged.

Storybook 7 vs 8

Frontguard supports both. The differences you'll feel:

FeatureStorybook 7.xStorybook 8.x
Index endpoint/stories.json/index.json (Frontguard falls back to /stories.json if absent)
Reported versionv7 in discovery logv8 in discovery log
play() wait signal'storyRendered' channel eventstoryRenders map + phase field
parameters.frontguardSupportedSupported
RecommendedIf you can upgrade, do — Storybook 8 has a faster preview and a real test API.Default for new projects.

If both endpoints return non-200, Frontguard logs:

Could not fetch Storybook index at http://localhost:6006. Is Storybook running?
Frontguard tried /index.json (Storybook 8) and /stories.json (Storybook 7).

The most common cause is Storybook still starting — wait for the manager built log line, or pass --ci to storybook dev to suppress the auto-open behaviour in CI.

CI recipe

The standard pattern: boot Storybook in headless / --ci mode, wait for it to listen, then run Frontguard against it.

.github/workflows/frontguard.yml
name: Frontguard

on:
  pull_request:
  push:
    branches: [main]

jobs:
  visual:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # required so the baseline orphan branch is reachable

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npx playwright install --with-deps chromium

      - name: Start Storybook
        run: npm run storybook -- --ci --quiet &
      - name: Wait for Storybook
        run: npx wait-on http://localhost:6006

      - name: Frontguard
        run: npx -p @frontguard/cli frontguard run
        env:
          # Optional: AI classification + auto-fix suggestions
          FRONTGUARD_OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: frontguard-report
          path: frontguard-report/

frontguard init --ci will scaffold this workflow for you (with the storybook command and port pre-filled when it detects a Storybook project).

Faster CI: prebuild Storybook

For larger projects, prebuilding to static HTML once per branch is faster than running storybook dev on every PR:

- name: Build Storybook
  run: npm run build-storybook
- name: Serve static Storybook
  run: npx http-server storybook-static -p 6006 &
- run: npx wait-on http://localhost:6006
- run: npx -p @frontguard/cli frontguard run

/index.json is included in the static build, so discovery still works.

vs Chromatic Visual Tests addon

The Chromatic Storybook addon is the closest thing in the ecosystem. Here's where Frontguard differs:

  • No per-snapshot fees. Chromatic charges by the snapshot beyond its free tier (5,000 snapshots/mo). Frontguard's cost is wherever your CI runs and (optionally) what you pay your AI provider — for most teams that's well under $0.01 per run.
  • AI classification, not just diff approval. Chromatic shows you the diff and asks you to accept or reject. Frontguard does that and tells you whether the change looks like a regression, an intentional update, or content drift — with a confidence score. For teams that grew tired of blind-approving 30 diffs every PR, that's the productivity win.
  • Auto-fix suggestions. When Frontguard's analysis identifies a CSS cause (overflow, spacing, z-index, responsive break, color shift), it ships you a patch. With verifyFixes: true it tests the patch in a sandbox before showing it to you.
  • Self-hosted baselines. Frontguard's baselines live in a git orphan branch, not a vendor's database. There is no lock-in: if Frontguard goes away tomorrow, your baselines stay in your repo.
  • Same tool for stories + full pages. Chromatic's pricing and product surface assume Storybook-only. Frontguard is built so the same config can capture stories, then point at a deployed preview URL to capture the marketing site. One tool, one report, one PR comment.
  • What Chromatic still does better: UI review workflow polish (their diff-approval UI has years of iteration on it), TurboSnap (their affected-story detection is excellent), and ecosystem credibility from the Storybook team. If those matter more than cost + AI, Chromatic is a fair choice — Frontguard ships a frontguard-vs-chromatic migration guide for the other case.

For competitive context see docs/research/competitive-analysis.md § 2 Chromatic.

Troubleshooting

"Storybook discovery returned 0 stories"

The most common cause is Storybook still starting. Confirm with:

curl -fsS http://localhost:6006/index.json | head

If that returns a JSON blob with an entries key, Frontguard should discover them. If it 404s, your Storybook version is too old to publish an index. Update to Storybook 7.6+ (preferably 8.x).

Stories with play() are captured pre-interaction

Frontguard waits up to pageTimeout (30s by default) for the play function to settle. If a story is captured pre-play, the wait probably timed out. Check the run log for Storybook ready-wait failed warnings and lengthen pageTimeout if your play() blocks on real network calls.

Per-story viewports aren't being honoured

Confirm the story's parameters are shaped exactly like:

parameters: {
  frontguard: {
    viewports: [768],
  },
},

The viewports key must be inside frontguard, not at the top of parameters. The Storybook viewports addon's parameters.viewport block is unrelated and not read by Frontguard.

"/index.json returns 404, /stories.json returns the index"

You're on Storybook 7.x without the v8 publisher. Frontguard already handles this — it falls back to /stories.json automatically. The log will show Storybook discovery (v7): .... If you want to silence the 404 noise in dev tools, upgrade Storybook or use the storybook-addon-v5-index publisher.

CI: Storybook starts but tests hit before it's ready

Add npx wait-on http://localhost:6006/index.json before the frontguard step. Waiting on the URL alone isn't enough — Storybook can be serving the manager UI before index.json is generated.

Worked example: the fixture

The Frontguard repo ships a runnable Storybook fixture at packages/cli/__fixtures__/storybook/. It exercises every code path documented above:

  • Two component stories files (Button.stories.tsx, Modal.stories.tsx).
  • A play()-driven story (Modal/OpenedByPlay).
  • Per-story parameters.frontguard overrides for viewports, threshold, and ignore rules.
  • A .storybook/main.ts + preview.ts pair for Storybook 8 (Vite builder).
  • A frontguard.config.ts you can run directly.

To boot the fixture:

cd packages/cli/__fixtures__/storybook
npm install
npm run storybook            # http://localhost:6006
# in another terminal
npx -p @frontguard/cli frontguard run --config ./frontguard.config.ts

Use it as a working reference whenever your own project's setup misbehaves.

Next steps

  • Quick Start — the 90-second tour of frontguard run, PR comments, and baselines.
  • CLI reference — every flag the CLI accepts, including --verify-fixes. AI analysis and baseline strategy are enabled in frontguard.config.ts (plus the relevant API-key env vars), not via CLI flags.
  • CI/CD recipes — patterns for GitHub Actions, GitLab CI, and Vercel preview integration.

On this page