Component-Driven Development with Storybook

21. February, 2026 10 min read Develop

Building UIs in Isolation

Developing UI components inside a running application means wrestling with state, routing, authentication, and data dependencies just to see a button in its loading state. Storybook removes all of that friction by providing a dedicated workshop where you build, test, and document components in complete isolation.

With over 80,000 GitHub stars, Storybook is the most widely adopted component development tool in the frontend ecosystem. It’s evolved far beyond a simple component explorer — modern Storybook is a full testing platform with interaction testing, visual regression, accessibility checks, and CI/CD integration built in. Whether you’re building a design system, a component library, or just want a better development workflow for your Next.js application, Storybook provides the infrastructure to develop with confidence.

In this post, we’ll set it up with Next.js and explore the features that make it essential for component-driven development.

Why Storybook?

The core idea is simple: every component state gets its own “story” — a rendered example with specific props, data, and context. Instead of clicking through your app to reach an edge case, you browse to it directly in Storybook.

This approach solves several problems:

  • Isolated development: Build components without wiring up the full application stack
  • Edge case coverage: Render loading states, error states, empty states, and overflow scenarios on demand
  • Living documentation: Auto-generated docs that stay in sync with your actual components
  • Team collaboration: Designers and PMs can browse a published Storybook without running code
  • Testing foundation: Stories become reusable test cases for visual, interaction, and accessibility tests

Setting Up with Next.js

The installer auto-detects your framework:

npx create storybook@latest

It offers a choice between Webpack (@storybook/nextjs) and Vite (@storybook/nextjs-vite) builders. Vite is recommended for faster builds and better testing support. The installer also generates example stories, a .storybook configuration directory, and adds the necessary scripts to your package.json.

Storybook handles Next.js-specific features automatically — next/image, next/font, next/navigation, CSS Modules, Tailwind CSS, and path aliases from tsconfig.json all work without configuration. This is a significant advantage over trying to render Next.js components in a generic Storybook setup, where you’d need to manually mock framework APIs and configure module resolution.

For Tailwind CSS projects, import your stylesheet in the Storybook preview:

// .storybook/preview.ts
import '../src/globals.css';

const preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

If you’re using shadcn/ui, components work directly since they’re standard React components with Tailwind classes. Just ensure your CSS variables from globals.css are imported.

Writing Stories

Stories use the Component Story Format (CSF), an ES module-based open standard. Each file has a default export describing the component and named exports for each story:

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

const meta = {
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      options: ['default', 'destructive', 'outline', 'ghost'],
      control: { type: 'select' },
    },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
  args: {
    children: 'Click me',
    variant: 'default',
  },
};

export const Destructive: Story = {
  args: {
    children: 'Delete',
    variant: 'destructive',
  },
};

export const Loading: Story = {
  args: {
    children: 'Saving...',
    disabled: true,
  },
};

Each named export becomes a story in Storybook’s sidebar. The args object maps directly to component props, and Storybook generates interactive controls automatically based on TypeScript types. Change a prop in the controls panel and the component re-renders instantly.

The satisfies Meta<typeof Button> pattern ensures type safety — if you pass an arg that doesn’t match the component’s props, TypeScript catches it at compile time. The tags: ['autodocs'] entry tells Storybook to generate a documentation page for this component, which we’ll cover in more detail later.

Stories can also define decorators — wrapper components that provide context like theme providers, layout wrappers, or mock data providers:

export const InSidebar: Story = {
  decorators: [
    (Story) => (
      <div style={{ width: 250, padding: 16 }}>
        <Story />
      </div>
    ),
  ],
  args: {
    children: 'Sidebar Action',
    variant: 'ghost',
  },
};

Decorators can be defined at the story level, the component level (in the meta), or globally in the preview configuration. This layered approach lets you set up common context once and override it where needed.

Args and Controls

Controls are Storybook’s interactive prop editor. They’re inferred from your TypeScript types — a boolean prop becomes a toggle, an enum becomes a select dropdown, a string becomes a text input.

You can customize controls through argTypes:

const meta = {
  component: Card,
  argTypes: {
    backgroundColor: { control: 'color' },
    padding: {
      control: { type: 'range', min: 0, max: 100, step: 4 },
    },
    size: {
      options: ['sm', 'md', 'lg'],
      control: { type: 'radio' },
    },
    icon: {
      control: false, // disable control for complex props
    },
  },
} satisfies Meta<typeof Card>;

Pattern matchers let you set global rules — any arg name ending in color automatically gets a color picker, any arg ending in Date gets a date picker. This eliminates repetitive argTypes configuration across stories.

Interaction Testing

Play functions turn stories into interactive tests. They execute after a story renders, simulating real user behavior:

import { expect, fn, userEvent, within } from 'storybook/test';

export const SubmitForm: Story = {
  args: {
    onSubmit: fn(),
  },
  play: async ({ canvas, args, step }) => {
    await step('Fill in the form', async () => {
      await userEvent.type(
        canvas.getByLabelText('Email'),
        'user@example.com'
      );
      await userEvent.type(
        canvas.getByLabelText('Password'),
        'secretpassword'
      );
    });

    await step('Submit', async () => {
      await userEvent.click(
        canvas.getByRole('button', { name: 'Sign In' })
      );
    });

    await expect(args.onSubmit).toHaveBeenCalledOnce();
  },
};

The canvas object provides Testing Library queries (getByRole, getByText, getByLabelText), and userEvent simulates realistic interactions — clicks, typing, hover, tab navigation, and keyboard events. The step function groups interactions into collapsible sections for readability.

The Interactions panel in Storybook provides step-through debugging with pause, resume, and rewind controls. You can walk through each interaction to pinpoint exactly where a test fails. This is particularly valuable for complex multi-step flows like checkout processes or multi-page forms, where traditional debugging would require navigating through the entire application state.

The fn() utility from storybook/test creates a mock function (powered by Vitest’s vi.fn()) that records all calls and arguments. You can assert on it just like any mock — checking call counts, specific arguments, or call order. Combined with within(canvas) for scoping queries to the story canvas, you get a complete testing toolkit that feels familiar if you’ve used Testing Library.

Since these tests use Vitest under the hood, they also run in CI without a browser — giving you fast feedback on every pull request. Run them with:

npx storybook test

This executes all play functions across all stories and reports failures with detailed error messages, including which step failed and what the expected vs. actual values were.

Visual Testing

Visual testing compares rendered pixels against baseline snapshots, catching CSS regressions that unit tests miss entirely — a changed margin, an overflowing container, a broken responsive layout.

Chromatic, built by the Storybook team, integrates directly:

npx storybook@latest add @chromatic-com/storybook

On each commit, Chromatic captures screenshots of every story across multiple browsers, compares them to baselines, and highlights visual changes in your PR. Intentional changes are accepted as new baselines; regressions are flagged for review.

The workflow integrates with GitHub Actions, GitLab CI, and other CI/CD platforms:

# .github/workflows/chromatic.yml
name: Visual Tests
on: push

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: npm ci
      - uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Accessibility Testing

The accessibility addon runs axe-core checks on every story, catching up to 57% of WCAG issues automatically:

npx storybook@latest add @storybook/addon-a11y

Once installed, an Accessibility panel appears for every story showing violations, passes, and incomplete checks. Issues are categorized by severity with links to remediation guidance.

You can configure rules per-component or per-story:

export const WithCustomA11y: Story = {
  parameters: {
    a11y: {
      config: {
        rules: [
          { id: 'color-contrast', enabled: true },
          { id: 'landmark-one-main', enabled: false },
        ],
      },
    },
  },
};

Combined with the Vitest integration, accessibility checks run in CI alongside your other tests, preventing a11y regressions from being merged.

Autodocs

Adding tags: ['autodocs'] to your meta generates a documentation page automatically. It renders every story alongside an interactive prop table extracted from your TypeScript types:

const meta = {
  component: Button,
  tags: ['autodocs'],
  parameters: {
    docs: {
      description: {
        component: 'Primary UI button with multiple variants and sizes.',
      },
    },
  },
} satisfies Meta<typeof Button>;

For custom documentation, Storybook supports MDX files that mix Markdown with live component examples. Doc Blocks like <Canvas>, <Controls>, <ArgTypes>, and <Source> give you building blocks for tailored documentation pages.

This makes Storybook a single source of truth for component documentation — it’s always up to date because it’s generated from the same code that renders the components. No more outdated Confluence pages or README files that fall out of sync the moment someone refactors a prop name.

Addons Ecosystem

Beyond the essentials, Storybook has a rich addon ecosystem:

  • Backgrounds: Switch background colors and toggle grid overlays for alignment testing
  • Viewport: Test responsive layouts across device presets (mobile, tablet, desktop)
  • Themes: Switch between light and dark mode using class-based or data-attribute decorators
  • Actions: Log event handler calls to verify callbacks fire correctly
  • Measure & Outline: CSS debugging tools for inspecting spacing and layout
  • Highlight: Programmatically highlight DOM elements

Addons are configured in .storybook/main.ts and most work out of the box with zero configuration.

Testing Strategy

Stories are portable — they can be imported into Playwright, Cypress, Jest, or Vitest for additional testing layers. This means a single story definition serves multiple purposes:

Test Type Tool What It Catches
Render tests Stories themselves Crashes, rendering errors
Interaction tests Play functions + Vitest Functional bugs, broken flows
Visual tests Chromatic UI regressions, layout shifts
Accessibility tests a11y addon (axe-core) WCAG violations
Snapshot tests Vitest Unexpected markup changes

This layered approach means you write the story once and get multiple levels of testing coverage from it. The story is the single source of truth for how a component should look and behave.

The portability of stories is one of Storybook’s most underappreciated features. Instead of maintaining separate test fixtures for each testing tool, you define the component state once in a story and import it everywhere. If you change how a component works, you update the story and all your tests automatically reflect the change.

For example, importing a story into a Playwright test for end-to-end testing:

import { test, expect } from '@playwright/test';

test('form submission works', async ({ page }) => {
  await page.goto('/storybook/iframe.html?id=forms-login--submit-form');
  await expect(page.getByText('Welcome back')).toBeVisible();
});

Conclusion

Storybook has evolved from a component explorer into a comprehensive development and testing platform. By building components in isolation, you catch issues earlier, document your UI automatically, and create a shared language between developers, designers, and stakeholders.

Combined with Tailwind CSS and shadcn/ui, Storybook fits naturally into the modern Next.js stack. The Vitest integration means your component tests run fast in CI, visual testing catches the CSS regressions that code reviews miss, and autodocs ensure your documentation never goes stale.

The initial setup takes minutes, and the return on investment grows with every component you add. If you’re building a component library or working on a team where UI consistency matters, Storybook is the tool that ties everything together.

‘Till next time!