Use Cases (Updated: 6/2/2026)

Storybook Development with Claude Code: Practical UI Catalogs and Tests

Build Storybook with Claude Code: setup, typed stories, interaction tests, CI, and Chromatic-ready review.

Storybook Development with Claude Code: Practical UI Catalogs and Tests

Storybook lets you develop UI components outside the main app. For beginners, it is a catalog where you can open a button, form, or card in isolation. In real projects, its value is bigger than a nice visual list: it records props, states, interactions, accessibility expectations, review links, and CI checks in one place.

If you ask Claude Code only to “add Storybook”, it may generate a few Button.stories.tsx files and stop there. That is rarely enough. A useful setup defines which components matter, which states must be represented, what the play function should test, and how visual review runs in CI with Chromatic or an equivalent workflow.

This guide uses React, TypeScript, and Vite with Storybook 8/9+ patterns: .storybook/main.ts, CSF with Meta/StoryObj, the satisfies operator, @storybook/test interaction tests, @storybook/addon-vitest, and a Chromatic-ready CI workflow. The technical references are the official main configuration, TypeScript stories, interaction testing, Vitest addon, and Chromatic for Storybook docs.

For adjacent work, pair this with Claude Code design system development and Claude Code accessibility implementation. Storybook becomes much more useful when it protects design decisions and user flows, not just screenshots.

Three Practical Use Cases to Start With

Do not add every UI at once. Start with components that change often and affect conversion, onboarding, or support.

Use caseStates to captureWork to delegate to Claude Code
Design system Buttonprimary, secondary, loading, disabled, all sizesProp types, stories, controls, autodocs
Free signup formempty, invalid input, submitting, successplay functions, mocks, validation checks
Article or product CTAdefault, highlighted, mobile width, long copyResponsive review, Chromatic diff, internal link checks

These three examples help designers review states, developers understand the props contract, and site owners confirm that conversion-related UI still works after a change.

Use a Vite-Based Setup

For Storybook’s Vitest addon, a Vite-based Storybook framework is the cleanest path. In a Next.js app, evaluate @storybook/nextjs-vite; in a React + Vite project, keep everything on @storybook/react-vite.

npm create vite@latest storybook-claude-demo -- --template react-ts
cd storybook-claude-demo
npm install
npm create storybook@latest
npx storybook add @storybook/addon-a11y
npx storybook add @storybook/addon-vitest
npm install -D chromatic @vitest/browser-playwright playwright
npx playwright install chromium

Keep local development, static build, Storybook tests, and visual publishing as separate scripts.

{
  "scripts": {
    "dev": "vite",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test-storybook": "vitest --project=storybook --run",
    "chromatic": "chromatic"
  }
}
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: '@storybook/react-vite',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-vitest'],
  docs: {
    defaultName: 'Docs',
  },
  staticDirs: ['../public'],
};

export default config;
// .storybook/preview.ts
import type { Preview } from '@storybook/react-vite';

const preview: Preview = {
  tags: ['autodocs'],
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    a11y: {
      test: 'todo',
    },
  },
};

export default preview;

Example 1: A Typed Button Story

Do not invent Storybook-only props. Stories should describe the real component contract used in production.

// src/components/Button.tsx
import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react';

type ButtonVariant = 'primary' | 'secondary' | 'outline';
type ButtonSize = 'sm' | 'md' | 'lg';

export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  children: ReactNode;
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
};

const variantStyle: Record<ButtonVariant, CSSProperties> = {
  primary: { background: '#2563eb', color: '#ffffff', borderColor: '#2563eb' },
  secondary: { background: '#0f172a', color: '#ffffff', borderColor: '#0f172a' },
  outline: { background: '#ffffff', color: '#0f172a', borderColor: '#94a3b8' },
};

const sizeStyle: Record<ButtonSize, CSSProperties> = {
  sm: { minHeight: 32, padding: '0 12px', fontSize: 14 },
  md: { minHeight: 40, padding: '0 16px', fontSize: 15 },
  lg: { minHeight: 48, padding: '0 20px', fontSize: 16 },
};

export function Button({
  children,
  variant = 'primary',
  size = 'md',
  loading = false,
  disabled,
  style,
  ...props
}: ButtonProps) {
  return (
    <button
      {...props}
      disabled={disabled || loading}
      aria-busy={loading || undefined}
      style={{
        ...variantStyle[variant],
        ...sizeStyle[size],
        borderWidth: 1,
        borderStyle: 'solid',
        borderRadius: 6,
        fontWeight: 700,
        cursor: disabled || loading ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.55 : 1,
        ...style,
      }}
    >
      {loading ? 'Sending...' : children}
    </button>
  );
}
// src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  args: {
    children: 'Start free trial',
    variant: 'primary',
    size: 'md',
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'outline'],
    },
    size: {
      control: 'inline-radio',
      options: ['sm', 'md', 'lg'],
    },
    loading: { control: 'boolean' },
    disabled: { control: 'boolean' },
  },
} satisfies Meta<typeof Button>;

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

export const Primary = {} satisfies Story;

export const Secondary = {
  args: {
    variant: 'secondary',
    children: 'View pricing',
  },
} satisfies Story;

export const AllSizes = {
  render: () => (
    <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
} satisfies Story;

export const Loading = {
  args: {
    loading: true,
  },
} satisfies Story;

The key instruction for Claude Code is not “make many stories”. Ask it to keep production props intact, cover long CTA labels, and check disabled and loading accessibility behavior.

Example 2: Interaction Tests for a Signup Form

The play function runs after a story renders. That lets a story verify behavior, not just appearance.

// src/components/SignupForm.tsx
import type { FormEvent } from 'react';
import { useState } from 'react';
import { Button } from './Button';

export type SignupFormData = {
  name: string;
  email: string;
  plan: 'free' | 'team';
};

type SignupFormProps = {
  plan?: SignupFormData['plan'];
  onSubmit?: (data: SignupFormData) => Promise<void> | void;
};

export function SignupForm({ plan = 'free', onSubmit }: SignupFormProps) {
  const [message, setMessage] = useState('');

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = new FormData(event.currentTarget);
    const data: SignupFormData = {
      name: String(form.get('name') ?? ''),
      email: String(form.get('email') ?? ''),
      plan,
    };

    if (!data.email.includes('@')) {
      setMessage('Please enter a valid email address.');
      return;
    }

    await onSubmit?.(data);
    setMessage('Thanks, we will send the setup guide.');
  }

  return (
    <form onSubmit={handleSubmit} style={{ display: 'grid', gap: 12, maxWidth: 360 }}>
      <label>
        Name
        <input name="name" required style={{ display: 'block', width: '100%', minHeight: 36 }} />
      </label>
      <label>
        Email
        <input name="email" type="email" required style={{ display: 'block', width: '100%', minHeight: 36 }} />
      </label>
      <Button type="submit">Get the guide</Button>
      {message ? <p role="status">{message}</p> : null}
    </form>
  );
}
// src/components/SignupForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, within } from '@storybook/test';
import { SignupForm } from './SignupForm';

const meta = {
  title: 'Components/SignupForm',
  component: SignupForm,
  args: {
    plan: 'team',
    onSubmit: fn(),
  },
} satisfies Meta<typeof SignupForm>;

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

export const Empty = {} satisfies Story;

export const InvalidEmail = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByLabelText('Name'), 'Masa');
    await userEvent.type(canvas.getByLabelText('Email'), 'masa.example.com');
    await userEvent.click(canvas.getByRole('button', { name: 'Get the guide' }));

    await expect(canvas.getByRole('status')).toHaveTextContent('valid email');
  },
} satisfies Story;

export const SuccessfulSubmit = {
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByLabelText('Name'), 'Masa');
    await userEvent.type(canvas.getByLabelText('Email'), 'masa@example.com');
    await userEvent.click(canvas.getByRole('button', { name: 'Get the guide' }));

    await expect(args.onSubmit).toHaveBeenCalledWith({
      name: 'Masa',
      email: 'masa@example.com',
      plan: 'team',
    });
    await expect(canvas.getByRole('status')).toHaveTextContent('setup guide');
  },
} satisfies Story;

This pattern works well for newsletter forms, free guide downloads, product CTA forms, and consultation requests because it checks the path closest to revenue.

Example 3: Vitest and Chromatic-Ready CI

The Vitest addon turns stories into component tests in a real browser environment for Vite-based Storybook projects. Stories with play functions run their interactions and assertions.

// .storybook/vitest.setup.ts
import { setProjectAnnotations } from '@storybook/react-vite';
import * as previewAnnotations from './preview';

setProjectAnnotations([previewAnnotations]);
// vitest.config.ts
import { defineConfig, defineProject, mergeConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import viteConfig from './vite.config';

const dirname = path.dirname(fileURLToPath(import.meta.url));

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      projects: [
        defineProject({
          extends: true,
          plugins: [
            storybookTest({
              configDir: path.join(dirname, '.storybook'),
              storybookScript: 'npm run storybook -- --ci',
              storybookUrl: 'http://localhost:6006',
              tags: {
                include: ['test'],
                exclude: ['experimental'],
              },
            }),
          ],
          test: {
            name: 'storybook',
            browser: {
              enabled: true,
              provider: playwright({}),
              headless: true,
              instances: [{ browser: 'chromium' }],
            },
            setupFiles: ['./.storybook/vitest.setup.ts'],
          },
        }),
      ],
    },
  }),
);
# .github/workflows/storybook.yml
name: storybook

on:
  pull_request:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run test-storybook
      - run: npm run build-storybook
      - name: Publish to Chromatic
        if: ${{ secrets.CHROMATIC_PROJECT_TOKEN != '' }}
        run: npx chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Prompt Template for Claude Code

Set up Storybook for a React + TypeScript + Vite project.

Scope:
- src/components/Button.tsx
- src/components/SignupForm.tsx

Requirements:
- Use @storybook/react-vite in .storybook/main.ts
- Use CSF with Meta/StoryObj and satisfies
- Add Button stories for primary, secondary, outline, loading, disabled, and all sizes
- Add SignupForm play functions for invalid email and successful submit
- Use within, userEvent, expect, and fn from @storybook/test
- Add Vitest addon configuration so test-storybook runs in CI
- Add build-storybook and a Chromatic-style visual check to GitHub Actions

Do not:
- Use pseudocode
- Add props that do not exist in the real component
- Hard-code secrets
- Format unrelated files

Report:
- Changed files
- Commands run
- Remaining risks

Common Pitfalls

First, avoid Storybook-only props. If stories need a state that the app cannot produce, fix the component contract instead of hiding the mismatch in Storybook.

Second, do not test only the happy path. Add invalid input, disabled states, loading states, duplicate submit protection, and long copy.

Third, stabilize visual diffs. Dates, random values, animations, skeleton loaders, and remote images can make Chromatic noisy.

Fourth, keep the framework package consistent. Mixing @storybook/react, @storybook/react-vite, and @storybook/nextjs-vite causes configuration and alias problems.

Fifth, install browsers in CI when using Vitest browser mode or Playwright.

Sixth, prioritize revenue-related UI. A perfect Button catalog is not enough if the article CTA, product card, or consultation form breaks.

Monetization Flow

For a technical media site, Storybook is also a revenue protection tool. Free guide forms, product page CTAs, training inquiry forms, and internal link cards can be represented as stories, tested through interactions, and reviewed visually on every PR.

Start with the free cheat sheet if you want reusable Claude Code request patterns. Use products and templates when you want a packaged workflow. For repository-specific Storybook, CI, Chromatic, accessibility, and CTA review, use Claude Code training and consulting.

What Happened When I Tried This

In a small React test project, the biggest time saver was not the Button variants but the failing SignupForm story. When I only tested the success path, invalid email handling and duplicate submit behavior slipped through. Splitting InvalidEmail and SuccessfulSubmit, then running test-storybook and build-storybook in CI, reduced the number of follow-up fixes I had to ask Claude Code for. Visual review also became easier when I focused on CTA and form stories instead of trying to screenshot every page.

#Claude Code #Storybook #UI development #components #frontend
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.