Storybook-Driven Development
Component-First, Not Page-First
Most developers build pages, then extract components later. This is backwards. You end up with components shaped by the page they first appeared on, coupled to specific data and layout assumptions.
Storybook-driven development flips the order: build and test the component in isolation first, then integrate it into pages. This is how Stripe, Shopify, and the BBC build their design systems.
Think of Storybook like a test track for cars. You do not test a new engine by putting it in a car and driving on the highway. You test it on a dynamometer in controlled conditions — varying RPM, load, temperature. Storybook is your component dynamometer. You test every prop combination, edge case, and interaction before the component ever touches a real page.
Writing Stories as Specifications
A story is not just a visual demo. It is a specification. Write stories before writing the component — they define what the component should do.
// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button";
const meta: Meta<typeof Button> = {
component: Button,
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "ghost", "destructive"],
},
size: {
control: "select",
options: ["sm", "md", "lg"],
},
disabled: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: "primary",
children: "Save Changes",
},
};
export const Secondary: Story = {
args: {
variant: "secondary",
children: "Cancel",
},
};
export const Destructive: Story = {
args: {
variant: "destructive",
children: "Delete Account",
},
};
export const Disabled: Story = {
args: {
variant: "primary",
disabled: true,
children: "Cannot Click",
},
};
export const Loading: Story = {
args: {
variant: "primary",
children: "Saving...",
disabled: true,
},
render: (args) => (
<Button {...args}>
<Spinner className="mr-2 h-4 w-4" />
Saving...
</Button>
),
};
These stories are written before the component exists. They define every visual state the component must support. The implementation then follows the spec.
Args, Controls, and Actions
Storybook gives you three interactive tools for exploring components:
Args and Controls
Args are the props you pass to a story. Controls generate UI widgets that let anyone (designers, PMs, QA) tweak props live.
const meta: Meta<typeof CourseCard> = {
component: CourseCard,
argTypes: {
difficulty: {
control: "radio",
options: ["beginner", "intermediate", "advanced"],
},
progress: {
control: { type: "range", min: 0, max: 100, step: 5 },
},
title: {
control: "text",
},
description: {
control: "text",
},
tags: {
control: "object",
},
},
};
export const WithProgress: Story = {
args: {
title: "React Internals Deep Dive",
description: "Understand the fiber architecture, reconciliation, and concurrent features.",
difficulty: "advanced",
progress: 65,
tags: ["react", "internals", "performance"],
},
};
A designer can now drag the progress slider from 0 to 100 and see exactly how the progress bar behaves at every value. No code changes needed.
Actions
Actions log interactions. They verify that events fire correctly without wiring up real handlers.
export const ClickTracking: Story = {
args: {
children: "Track Me",
onClick: fn(),
},
};
The fn() function from Storybook intercepts the click and logs it in the Actions panel. You see exactly what arguments the callback received.
Interaction Testing with Play Functions
Storybook is not just for visual testing. Play functions let you write interactive tests that run inside the story:
import { expect, userEvent, within } from "@storybook/test";
export const TabSwitching: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const codeTab = canvas.getByRole("tab", { name: "Code" });
const previewTab = canvas.getByRole("tab", { name: "Preview" });
await expect(codeTab).toHaveAttribute("aria-selected", "true");
await expect(canvas.getByText("function hello()")).toBeVisible();
await userEvent.click(previewTab);
await expect(previewTab).toHaveAttribute("aria-selected", "true");
await expect(codeTab).toHaveAttribute("aria-selected", "false");
await expect(canvas.getByText("Hello World")).toBeVisible();
},
};
export const KeyboardNavigation: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const firstTab = canvas.getByRole("tab", { name: "Code" });
await userEvent.click(firstTab);
await userEvent.keyboard("{ArrowRight}");
await expect(canvas.getByRole("tab", { name: "Preview" })).toHaveFocus();
await userEvent.keyboard("{ArrowRight}");
await expect(canvas.getByRole("tab", { name: "Tests" })).toHaveFocus();
await userEvent.keyboard("{Enter}");
await expect(canvas.getByRole("tab", { name: "Tests" })).toHaveAttribute(
"aria-selected",
"true",
);
},
};
These tests verify real user interactions: clicking tabs, keyboard navigation, ARIA state changes. They run in a real browser, not jsdom. This catches bugs that unit tests miss.
- 1Write stories before implementation — they are your spec, not your demo
- 2Create edge case stories: empty data, long text, loading, error, disabled states
- 3Use play functions for interaction testing — they run in a real browser
- 4Use Controls to let non-engineers explore component variations
- 5Tag stories with autodocs to auto-generate documentation
Visual Regression Testing
The real power of Storybook at scale: visual regression testing. Storybook 9 (June 2025) introduced Storybook Test — unified interaction, accessibility, and visual testing built right in. You can run visual regression locally without a cloud service. For teams that need cloud-based visual regression across browsers and viewports, Chromatic (built by the Storybook team) and Percy remain solid options. Storybook 10 (2026) added CSF Factories for simpler story authoring and MCP support for AI-assisted development.
This catches the bugs that unit tests and integration tests miss: subtle spacing changes, color shifts, font rendering differences, z-index stacking issues.
# Storybook 9+ built-in visual testing — no cloud service needed
npx storybook test --visual
# Cloud-based option — Chromatic for cross-browser visual diffs
npx chromatic --project-token=$CHROMATIC_TOKEN
Storybook as Living Documentation
Static documentation goes stale the moment it is written. Storybook stories are always current because they render the actual component code:
const meta: Meta<typeof Tooltip> = {
component: Tooltip,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component:
"A tooltip displays informative text when users hover or focus an element. " +
"Built on Radix UI Tooltip with full keyboard and screen reader support.",
},
},
},
};
export const Default: Story = {
args: {
content: "This action cannot be undone",
children: <Button variant="destructive">Delete</Button>,
},
parameters: {
docs: {
description: {
story: "The default tooltip appears on hover after a 200ms delay.",
},
},
},
};
export const WithCustomDelay: Story = {
args: {
content: "Saves your progress",
delayDuration: 0,
children: <Button>Save</Button>,
},
parameters: {
docs: {
description: {
story: "Set delayDuration={0} for tooltips on critical actions where immediate feedback matters.",
},
},
},
};
With autodocs enabled, Storybook auto-generates a documentation page from your stories: prop table (extracted from TypeScript types), live examples, and controls.
The Component Development Workflow
| What developers do | What they should do |
|---|---|
| Writing one Default story per component and calling it done A single story catches zero edge cases. The value of Storybook is exhaustive visual coverage. If you only have one story, you are not using Storybook — you are using a demo page. | Write stories for every prop combination, state, and edge case (empty, error, loading, long text) |
| Testing component logic in Storybook play functions instead of unit tests Play functions run in a real browser, which is slow and flaky for pure logic tests. Use them for what they are best at: testing real DOM interactions and visual states. | Use play functions for interaction testing (clicks, keyboard nav, ARIA). Use Vitest for pure logic. |
| Keeping Storybook as a dev-only tool that nobody else sees Storybook's greatest value is as a communication tool. When designers can see every component state live, review cycles shrink dramatically. Deploy it. | Deploy Storybook as a shared reference for designers, PMs, and QA engineers |
You are tasked with setting up a component development workflow for a team of 15 engineers and 3 designers. The design system has 120 components. How would you structure Storybook? What stories would be mandatory for every component? How would you integrate visual regression testing into the CI pipeline? How would you handle theme and viewport variations?