Skip to content

Storybook-Driven Development

advanced15 min read

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.

Mental Model

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.

Quiz
What is the primary benefit of writing Storybook stories before implementing the component?

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.

Quiz
A designer reports that a CourseCard looks broken when the title is very long. How should you handle this in Storybook?

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.

Key Rules
  1. 1Write stories before implementation — they are your spec, not your demo
  2. 2Create edge case stories: empty data, long text, loading, error, disabled states
  3. 3Use play functions for interaction testing — they run in a real browser
  4. 4Use Controls to let non-engineers explore component variations
  5. 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
Quiz
Visual regression tests flag a 1-pixel border color change in a Button story. The change was intentional (design token update). What should you do?

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

Execution Trace
1. Write Stories
Define every state the component needs: default, loading, error, disabled, edge cases
Stories are your spec
2. Implement Component
Build the component to satisfy every story. Red-green-refactor for visuals.
Storybook hot-reloads as you code
3. Add Play Functions
Write interaction tests for keyboard nav, clicks, focus management
Tests run in a real browser
4. Visual Review
Deploy Storybook preview. Designer reviews every story.
Chromatic auto-diffs against baseline
5. Integrate
Import the battle-tested component into the page
No surprises — it is already verified
What developers doWhat 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
Quiz
Your team has 200 components in Storybook but visual regression tests take 45 minutes to run. How do you reduce the CI time?
Interview Question

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?