MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Storybook 7: Component Development Environment

Use Storybook: stories, addons, interaction testing, and design system documentation.

StorybookComponentsDesign SystemsFrontend

By MinhVo

Introduction

Building UI components in isolation is one of the most effective ways to improve frontend development speed and quality. Yet most developers still build components by navigating through their application to reach the right state — clicking through forms, scrolling to modals, or logging in to see authenticated content. This workflow is slow, fragile, and makes it impossible to test edge cases systematically.

Storybook solves this by providing a dedicated development environment for UI components. You define "stories" — snapshots of a component in a specific state — and Storybook renders them in an interactive playground where you can tweak props, test different viewport sizes, and verify accessibility without touching your application code.

Storybook 7 represents a major evolution: it uses Vite for instant hot module replacement, supports CSF3 (Component Story Format 3) for cleaner story definitions, includes built-in interaction testing, and offers first-class TypeScript support. With over 800 integrations and addons, it has become the industry standard for component development and design system documentation.

This guide walks through setting up Storybook 7, writing effective stories, using addons for accessibility and interaction testing, and integrating Storybook into your design system workflow.

Component Development

Understanding Storybook 7: Core Concepts

Storybook is a development-time tool that runs alongside your application. It discovers story files in your codebase, renders each story in an iframe, and provides a UI for browsing, searching, and interacting with your component library.

Stories

A story represents a single state of a component. A Button component might have stories for default, primary, disabled, loading, and icon-only states. Each story is a function that returns a rendered component with specific props.

CSF3 (Component Story Format 3)

CSF3 is the latest story format, using objects instead of functions:

// Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
 
const meta = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
} satisfies Meta<typeof Button>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Click Me',
  },
};
 
export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Disabled',
  },
};

Addons

Addons extend Storybook's capabilities. The essential addons are:

  • Controls: Interactive prop editing in the UI
  • Actions: Log events (onClick, onChange) in the actions panel
  • Accessibility: Run axe-core checks on each story
  • Interactions: Write and run play functions that simulate user behavior
  • Docs: Auto-generate documentation from stories and component metadata

Autodocs

The tags: ['autodocs'] annotation tells Storybook to automatically generate a documentation page for each component. This page shows the component's props table, description, and all stories — serving as living documentation that stays in sync with the code.

Architecture and Design Patterns

Storybook Architecture

┌─────────────────────────────────┐
│        Storybook UI             │
│   (React-based sidebar/preview) │
├─────────────────────────────────┤
│        Preview iframe           │
│   (Renders your components)     │
├─────────────────────────────────┤
│        Vite Builder             │
│   (Bundles stories + addons)    │
├─────────────────────────────────┤
│     Your Component Library      │
│   (React/Vue/Svelte/Angular)    │
└─────────────────────────────────┘

Storybook runs two processes: the manager (the UI shell) and the preview (the iframe that renders components). The manager communicates with the preview via a channel API.

File Organization

src/
  components/
    Button/
      Button.tsx
      Button.test.tsx
      Button.stories.tsx    ← Story file lives next to the component
    Card/
      Card.tsx
      Card.test.tsx
      Card.stories.tsx
  pages/
    ...
.storybook/
  main.ts              ← Storybook configuration
  preview.ts           ← Global decorators and parameters

Configuration

// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
 
const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@storybook/addon-viewport',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  typescript: {
    reactDocgen: 'react-docgen-typescript',
  },
};
 
export default config;
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css';
 
const preview: Preview = {
  parameters: {
    controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } },
    viewport: { viewports: { mobile: { name: 'Mobile', styles: { width: '375px', height: '812px' } } } },
  },
};
 
export default preview;

Design Systems

Step-by-Step Implementation

Setting Up Storybook

# Initialize Storybook in a React project
npx storybook@latest init
 
# Or for specific framework
npx storybook@latest init --type react_vite

This installs dependencies, adds scripts to package.json, and creates the .storybook configuration directory.

Writing Stories with CSF3

Let's build a complete Button component with comprehensive stories:

// src/components/Button/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from 'react';
 
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
  leftIcon?: ReactNode;
  rightIcon?: ReactNode;
}
 
export function Button({
  variant = 'secondary',
  size = 'md',
  loading = false,
  leftIcon,
  rightIcon,
  children,
  disabled,
  ...props
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled || loading}
      {...props}
    >
      {loading && <span className="spinner" />}
      {!loading && leftIcon && <span className="icon-left">{leftIcon}</span>}
      {children}
      {rightIcon && <span className="icon-right">{rightIcon}</span>}
    </button>
  );
}
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
 
const meta = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  args: {
    onClick: fn(),
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'danger', 'ghost'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
    },
  },
} satisfies Meta<typeof Button>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Primary Button',
  },
};
 
export const Secondary: Story = {
  args: {
    variant: 'secondary',
    children: 'Secondary Button',
  },
};
 
export const Danger: Story = {
  args: {
    variant: 'danger',
    children: 'Delete',
  },
};
 
export const Small: Story = {
  args: {
    size: 'sm',
    children: 'Small',
  },
};
 
export const Large: Story = {
  args: {
    size: 'lg',
    children: 'Large Button',
  },
};
 
export const Loading: Story = {
  args: {
    loading: true,
    children: 'Saving...',
  },
};
 
export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Disabled',
  },
};
 
export const WithLeftIcon: Story = {
  args: {
    leftIcon: '🚀',
    children: 'Launch',
  },
};
 
export const AllVariants: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem' }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="danger">Danger</Button>
      <Button variant="ghost">Ghost</Button>
    </div>
  ),
};

Interaction Testing with Play Functions

Play functions simulate user interactions and assert outcomes:

// src/components/LoginForm/LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { LoginForm } from './LoginForm';
 
const meta = {
  title: 'Components/LoginForm',
  component: LoginForm,
  tags: ['autodocs'],
} satisfies Meta<typeof LoginForm>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const EmptyForm: Story = {};
 
export const WithValidation: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // Try submitting empty form
    const submitButton = canvas.getByRole('button', { name: /sign in/i });
    await userEvent.click(submitButton);
 
    // Expect validation errors
    await expect(canvas.getByText(/email is required/i)).toBeInTheDocument();
    await expect(canvas.getByText(/password is required/i)).toBeInTheDocument();
  },
};
 
export const SuccessfulLogin: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // Fill in the form
    const emailInput = canvas.getByLabelText(/email/i);
    const passwordInput = canvas.getByLabelText(/password/i);
 
    await userEvent.clear(emailInput);
    await userEvent.type(emailInput, 'user@example.com');
    await userEvent.clear(passwordInput);
    await userEvent.type(passwordInput, 'securePassword123');
 
    // Submit
    const submitButton = canvas.getByRole('button', { name: /sign in/i });
    await userEvent.click(submitButton);
 
    // Assert success state
    await expect(canvas.getByText(/welcome/i)).toBeInTheDocument();
  },
};
 
export const FailedLogin: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    await userEvent.type(canvas.getByLabelText(/email/i), 'wrong@example.com');
    await userEvent.type(canvas.getByLabelText(/password/i), 'wrongpassword');
    await userEvent.click(canvas.getByRole('button', { name: /sign in/i }));
 
    await expect(canvas.getByText(/invalid credentials/i)).toBeInTheDocument();
  },
};

Building a Design System Page

Storybook can render full design system documentation alongside individual stories:

// src/docs/Colors.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
 
const colorTokens = {
  '--color-primary': '#3B82F6',
  '--color-secondary': '#6B7280',
  '--color-success': '#10B981',
  '--color-warning': '#F59E0B',
  '--color-danger': '#EF4444',
  '--color-background': '#FFFFFF',
  '--color-surface': '#F9FAFB',
  '--color-text': '#111827',
  '--color-text-muted': '#6B7280',
};
 
function ColorPalette() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
      {Object.entries(colorTokens).map(([name, value]) => (
        <div key={name} style={{ textAlign: 'center' }}>
          <div
            style={{
              backgroundColor: value,
              width: '100%',
              height: '80px',
              borderRadius: '8px',
              border: '1px solid #e5e7eb',
            }}
          />
          <p style={{ fontWeight: 'bold', marginTop: '0.5rem' }}>{name}</p>
          <p style={{ color: '#6b7280', fontSize: '0.875rem' }}>{value}</p>
        </div>
      ))}
    </div>
  );
}
 
const meta = {
  title: 'Design System/Colors',
  component: ColorPalette,
  tags: ['autodocs'],
} satisfies Meta<typeof ColorPalette>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Palette: Story = {};

Testing Workflow

Real-World Use Cases

Use Case 1: Component Library Documentation

Storybook serves as the single source of truth for a design system:

// src/components/Input/Input.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { Input } from './Input';
 
const meta = {
  title: 'Components/Input',
  component: Input,
  tags: ['autodocs'],
  parameters: {
    docs: {
      description: {
        component:
          'A flexible input component with label, helper text, error states, and icon support.',
      },
    },
  },
  argTypes: {
    type: {
      control: 'select',
      options: ['text', 'email', 'password', 'number', 'tel', 'url'],
    },
  },
} satisfies Meta<typeof Input>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {
  args: { label: 'Email', placeholder: 'Enter your email' },
};
 
export const WithError: Story = {
  args: {
    label: 'Email',
    value: 'invalid',
    error: 'Please enter a valid email address',
  },
};
 
export const WithHelperText: Story = {
  args: {
    label: 'Password',
    type: 'password',
    helperText: 'Must be at least 8 characters',
  },
};
 
export const Interactive: Story = {
  args: { label: 'Username', placeholder: 'Choose a username' },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const input = canvas.getByLabelText('Username');
 
    await userEvent.type(input, 'johndoe');
    await expect(input).toHaveValue('johndoe');
  },
};

Use Case 2: Visual Regression Testing

Integrate Chromatic for visual regression testing:

npm install -D chromatic
npx chromatic --project-token=<your-token>
// In CI pipeline
// .github/workflows/chromatic.yml
// Runs on every PR, comparing visual snapshots against the baseline

Use Case 3: Accessibility-First Development

The a11y addon catches accessibility issues during development:

// src/components/Modal/Modal.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Modal } from './Modal';
 
const meta = {
  title: 'Components/Modal',
  component: Modal,
  tags: ['autodocs'],
  parameters: {
    a11y: {
      config: {
        rules: [
          { id: 'color-contrast', enabled: true },
          { id: 'aria-dialog-name', enabled: true },
        ],
      },
    },
  },
} satisfies Meta<typeof Modal>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {
  args: {
    title: 'Confirm Action',
    children: 'Are you sure you want to proceed?',
    open: true,
  },
};

Best Practices for Production

  1. Co-locate stories with components: Place .stories.tsx files next to their components. This makes stories easy to find and maintain, and keeps them in sync with component changes.

  2. Use CSF3 with typed args: The satisfies Meta<typeof Component> pattern gives you type-safe args and automatic argTypes inference from TypeScript props.

  3. Tag stories with 'autodocs': Enable automatic documentation generation for every component. This creates a documentation page with props table, descriptions, and all stories.

  4. Write interaction tests for critical paths: Use play functions to test form submissions, modal interactions, and multi-step flows. These run in the browser and catch integration issues.

  5. Use global decorators for shared context: Add providers (theme, router, i18n) as global decorators in .storybook/preview.ts so every story has the necessary context.

  6. Organize with a consistent title hierarchy: Use a naming convention like 'Components/Forms/Input' to create a logical sidebar structure.

  7. Leverage Controls for prop exploration: Configure argTypes with controls (select, color, range) to make the controls panel intuitive for designers and product managers.

  8. Run Storybook in CI: Build Storybook as a static site in CI and deploy it. This ensures stories always compile and provides a reviewable artifact for PRs.

Common Pitfalls and Solutions

PitfallImpactSolution
Stories without interaction testsStories show UI but do not verify behaviorAdd play functions for critical stories
Global CSS conflictsStories look different from productionScope CSS or use CSS modules
Missing decoratorsComponents fail to render due to missing contextAdd required providers as decorators
Too many stories per componentSlow Storybook, hard to navigateUse Storybook's test utility for exhaustive testing, keep UI stories minimal
Outdated storiesStories no longer match component APICI catches this — add TypeScript strict mode
Large bundle sizeSlow Storybook startupUse Vite builder (default in v7), tree-shake unused addons

Performance Optimization

Storybook 7 with Vite provides excellent development performance. Additional optimizations:

// .storybook/main.ts
const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  framework: {
    name: '@storybook/react-vite',
    options: {
      builder: {
        viteConfigPath: '.storybook/vite.config.ts',
      },
    },
  },
  // Lazy-load heavy addons
  addons: [
    '@storybook/addon-essentials',
    {
      name: '@storybook/addon-a11y',
      options: { runManually: true }, // Only run on demand
    },
  ],
};

For large codebases, use story globs to limit what Storybook loads:

// Only load stories from the components directory
stories: ['../src/components/**/*.stories.@(ts|tsx)']

Testing Strategies

Storybook 7 includes a test runner that executes play functions as integration tests:

# Install the test runner
npm install -D @storybook/test-runner
 
# Run all interaction tests
npx test-storybook
 
# Run with coverage
npx test-storybook --coverage
// src/components/TodoList/TodoList.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { TodoList } from './TodoList';
 
const meta = {
  title: 'Components/TodoList',
  component: TodoList,
} satisfies Meta<typeof TodoList>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const AddAndCompleteItems: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // Add a todo
    const input = canvas.getByPlaceholderText(/add a todo/i);
    await userEvent.type(input, 'Buy groceries');
    await userEvent.keyboard('{Enter}');
 
    // Verify it appears
    await expect(canvas.getByText('Buy groceries')).toBeInTheDocument();
 
    // Complete it
    const checkbox = canvas.getByRole('checkbox', { name: /buy groceries/i });
    await userEvent.click(checkbox);
 
    // Verify completed state
    await expect(checkbox).toBeChecked();
  },
};

Future Outlook

Storybook 8 is in development with improved portable stories (stories that work outside Storybook), better React Server Components support, and a new test infrastructure. The Storybook team is also building first-class support for visual testing without third-party tools.

The convergence of Storybook with design tools (Figma integration via Storybook Connect) is creating a seamless design-to-code workflow. Designers can browse the component library in Figma, see which components are available, and verify that implementations match designs.

The trend toward "docs-first" development — where component documentation drives both design and implementation — positions Storybook as the central hub for frontend teams.

Conclusion

Storybook 7 is the definitive tool for component development and design system documentation. The key takeaways are:

  1. Stories capture every state of a component, enabling isolated development and testing
  2. CSF3 with TypeScript provides type-safe, concise story definitions
  3. Play functions enable interaction testing that runs alongside your stories
  4. Autodocs generates living documentation from your story definitions
  5. The accessibility addon catches a11y issues during development, not in production
  6. Storybook 7's Vite builder provides instant feedback during development
  7. Co-locating stories with components keeps documentation in sync with code

Start by adding stories to your most complex components — the ones with multiple states, conditional rendering, and user interactions. The clarity gained from seeing every component state in isolation will immediately improve your development workflow.