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.
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;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_viteThis 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 = {};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 baselineUse 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
-
Co-locate stories with components: Place
.stories.tsxfiles next to their components. This makes stories easy to find and maintain, and keeps them in sync with component changes. -
Use CSF3 with typed args: The
satisfies Meta<typeof Component>pattern gives you type-safe args and automatic argTypes inference from TypeScript props. -
Tag stories with 'autodocs': Enable automatic documentation generation for every component. This creates a documentation page with props table, descriptions, and all stories.
-
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.
-
Use global decorators for shared context: Add providers (theme, router, i18n) as global decorators in
.storybook/preview.tsso every story has the necessary context. -
Organize with a consistent title hierarchy: Use a naming convention like
'Components/Forms/Input'to create a logical sidebar structure. -
Leverage Controls for prop exploration: Configure argTypes with controls (select, color, range) to make the controls panel intuitive for designers and product managers.
-
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
| Pitfall | Impact | Solution |
|---|---|---|
| Stories without interaction tests | Stories show UI but do not verify behavior | Add play functions for critical stories |
| Global CSS conflicts | Stories look different from production | Scope CSS or use CSS modules |
| Missing decorators | Components fail to render due to missing context | Add required providers as decorators |
| Too many stories per component | Slow Storybook, hard to navigate | Use Storybook's test utility for exhaustive testing, keep UI stories minimal |
| Outdated stories | Stories no longer match component API | CI catches this — add TypeScript strict mode |
| Large bundle size | Slow Storybook startup | Use 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:
- Stories capture every state of a component, enabling isolated development and testing
- CSF3 with TypeScript provides type-safe, concise story definitions
- Play functions enable interaction testing that runs alongside your stories
- Autodocs generates living documentation from your story definitions
- The accessibility addon catches a11y issues during development, not in production
- Storybook 7's Vite builder provides instant feedback during development
- 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.