Storybook 8 with React: Setup, Stories and Visual Testing
Set up Storybook 8 with React from scratch, write Component Story Format 3 stories, and run visual regression tests — a practical guide for component library authors.
Why Storybook 8 Is Worth the Upgrade
Storybook 7 was already good. Storybook 8, released in early 2024, is the version you actually want to be on — it ships a native Vite builder by default, drops the old Webpack 4 dependency chain, and cuts cold-start time roughly in half on most projects. If you're still running 6.x, your node_modules is probably carrying around 300MB of dead weight.
The big headliner is first-class React Server Component support. It's still experimental as of mid-2026, but the architecture is there. More immediately useful: Storybook 8 standardises on Component Story Format 3 (CSF3) everywhere, which means satisfies TypeScript inference, cleaner play functions, and story-level tags you can use to filter your test runs. Honestly, CSF3 alone is reason enough to migrate.
That said, the migration from 7 to 8 is genuinely smooth. The storybook upgrade command handles most of it automatically, and the team has a solid migration guide that flags the handful of breaking changes. For most React projects you'll be done in under an hour.
One more thing — if you're building a component library (say, something design-system-flavoured like what you'd publish alongside Empire UI), having a running Storybook is table stakes. It's how you document, isolate, and test components without spinning up an entire application every time.
Installing Storybook 8 in a React Project
Start from an existing React project — Vite, Next.js, Create React App, whatever. Run the initialiser and let it detect your framework:
npx storybook@latest initThat one command does a lot: installs the right addons, creates .storybook/main.ts and .storybook/preview.ts, drops example stories into src/stories/, and adds storybook and build-storybook scripts to your package.json. On a Vite-based project it'll pick the Vite builder automatically. On Next.js 14+ it defaults to the @storybook/nextjs framework package, which handles the Next.js module resolution quirks for you.
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx|js|jsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;Worth noting: the addon-essentials bundle pulls in docs, controls, actions, backgrounds, viewport, and toolbars all at once. You don't need to list them individually. If you're tight on bundle size for the Storybook build itself, you can cherry-pick, but for 99% of projects just use essentials.
Then run npm run storybook and you'll get a dev server on port 6006. That's it. The initial setup genuinely takes about 90 seconds on a modern machine.
Writing CSF3 Stories That Actually Help You
CSF3 is the story format introduced in Storybook 6.4 and made the only format in 8. The mental model: a story file exports a default Meta object that describes the component, and then named exports for each variant. Each named export is a StoryObj — a plain object, not a function.
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'ghost', 'danger'],
},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me',
},
};
export const Ghost: Story = {
args: {
variant: 'ghost',
children: 'Cancel',
},
};The satisfies Meta<typeof Button> bit is the TypeScript 4.9+ trick that gives you full inference on argTypes without widening the type. If you're on an older TS version, as Meta<typeof Button> still works fine. Quick aside: the tags: ['autodocs'] line on the meta object automatically generates a documentation page for that component — you don't need to write MDX for every component anymore.
In practice, the args model is the thing that makes Storybook actually useful day-to-day. You define your component's props as args, and the Controls panel in the sidebar lets you tweak them live. Every state you care about — loading, disabled, error, truncated text — should be its own named story. If you only have a Default story, you're leaving 80% of the value on the table.
Look, don't overthink story organisation either. Keep story files next to the component files (Button.stories.tsx next to Button.tsx). The title in meta is just for sidebar grouping; you can nest with slashes like 'Design System/Forms/Button'. That said, don't go more than three levels deep or your sidebar becomes a nightmare.
Play Functions and Interaction Testing
Here's the feature that turns Storybook from a catalogue into an actual test runner: play functions. You write user interactions directly inside a story, and the @storybook/addon-interactions addon replays them in the canvas and shows you pass/fail state. It uses the Testing Library API, so the ergonomics are familiar.
import { userEvent, within, expect } from '@storybook/test';
export const FormSubmit: Story = {
args: {
onSubmit: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await userEvent.type(
canvas.getByLabelText('Email'),
'user@example.com'
);
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));
await expect(args.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ email: 'user@example.com' })
);
},
};The fn() import comes from @storybook/test — it's a Storybook-native mock that hooks into the Actions panel so you can see calls logged there. Since Storybook 8 ships @storybook/test as a thin wrapper around Vitest's test utilities, you get the full expect API you'd have in a unit test. No separate mock library needed.
You can run all your play functions headlessly with storybook test --coverage. This executes every story that has a play function against an actual browser (Chromium by default) using Playwright under the hood. It's a very different beast from Jest + jsdom — your CSS actually renders, your 16px font-size matters, and layout-dependent bugs surface immediately.
In practice this is where Storybook earns its keep for component libraries. You can write the story, verify it visually in the dev server, then have CI run the same scenario against a real browser on every PR. That feedback loop is genuinely tight.
Visual Regression Testing with Chromatic
Play functions catch interaction bugs. Visual regression testing catches the subtler stuff — a 2px margin change, a font-weight drift after a dependency bump, a colour token that got accidentally overwritten. Chromatic is the first-party solution here; it's made by the same team as Storybook and integrates with zero config.
npm install --save-dev chromatic
npx chromatic --project-token=<your-token> --build-script-name=build-storybookOn first run it baselines every story as a screenshot. On subsequent runs (your CI pipeline) it diffs pixel-by-pixel and flags changes for review. You either accept the diff (new baseline) or reject it (bug). The review UI shows before/after overlays and highlights exactly which pixels changed. It's hard to overstate how much time this saves when you're maintaining a component library across multiple themes.
Worth noting: Chromatic's free tier covers 5,000 snapshots per month, which is plenty for most open-source or small-team projects. If you're building a commercial design system and hitting the limits, look at the self-hosted @chromatic-com/storybook visual test addon — it's newer but lets you store snapshots yourself. The Empire UI component library, for instance, spans glassmorphism, neumorphism, claymorphism, and several other style families, which means dozens of visual variants per component that all need regression coverage.
One gotcha: make sure you're running Chromatic with --only-changed in branches and --auto-accept-changes on main to avoid a flood of pending snapshots. And set up your CI to block merges on unreviewed changes — otherwise teams quietly stop looking at the diffs.
Autodocs and MDX for Component Documentation
The tags: ['autodocs'] you saw earlier generates a documentation page from your component's TypeScript props and JSDoc comments. It's not perfect — complex union types sometimes render oddly — but for 80% of components it produces useful, always-up-to-date docs without any extra work.
When you need more control, reach for MDX. Storybook 8 uses MDX 3, which supports import statements properly and integrates with the @storybook/blocks package for inline story rendering.
import { Meta, Story, Controls, Canvas } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
# Button
Use the `Button` component for all primary and secondary actions.
Don't use it for navigation — that's what `Link` is for.
<Canvas of={ButtonStories.Primary} />
<Controls of={ButtonStories.Primary} />The Canvas block renders the live, interactive story inline in your docs page. Controls appears below it. This gives you documentation that's actually runnable — designers can open the Storybook URL and fiddle with props without touching code. That's a qualitatively different thing from a static screenshot in Notion.
Quick aside: if you're publishing your Storybook publicly (Chromatic does this automatically), your component documentation becomes a shareable URL. Teams that do this well link their Storybook from their npm README and use it as the canonical component API reference. It's a pattern worth stealing.
Integrating Storybook Into Your CI Pipeline
A Storybook that only runs locally is a nice-to-have. One that runs in CI on every PR is infrastructure. Here's a minimal GitHub Actions setup that builds Storybook, runs interaction tests, and uploads to Chromatic.
# .github/workflows/storybook.yml
name: Storybook
on: [push, pull_request]
jobs:
test:
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
- name: Run interaction tests
run: npx storybook test --ci
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: trueThe fetch-depth: 0 on the checkout action is important — Chromatic uses git history to detect which stories changed and skip unchanged baselines. Without full history, it re-baselines everything on every run, which burns through your snapshot quota fast.
Honestly, the storybook test --ci step is the more valuable of the two for day-to-day development. It runs all your play functions in a headless Playwright browser, exits with a non-zero code if anything fails, and prints clear output about which story and which assertion failed. Your team will catch broken states before they ever merge.
One more thing — if your project has multiple packages (a monorepo with a design system package and an app package), consider running one Storybook per package rather than one shared Storybook. The build and test times stay manageable, and teams own their own documentation. The tradeoff is a bit more infrastructure, but for anything beyond a handful of components it's worth it. For reference, Empire UI's component templates span multiple industry verticals and style systems — exactly the kind of setup where per-package Storybooks start paying dividends.
FAQ
Yes, install @storybook/nextjs and it handles App Router conventions. RSC support is still experimental in mid-2026, but client components work perfectly out of the box.
Play functions run in a real browser with your actual CSS applied — not jsdom. They're better for catching visual and layout bugs, but slower than Jest. Use both.
Yes. The @chromatic-com/storybook addon supports self-hosted snapshots, and tools like Percy or Playwright's screenshot assertions work too. Chromatic is just the zero-config path.
Wrap them in a decorator inside .storybook/preview.ts. Export a decorators array with your provider tree and every story inherits it automatically.