React Storybook: Component Documentation and Testing (2026)
Storybook is the industry-standard tool for building UI components in isolation. It gives you a live component catalog, interactive controls, accessibility auditing, and a visual regression testing pipeline — all without spinning up your full application. Storybook 8 (2024+) ships with Vite by default and makes the CSF3 format the standard.
Table of Contents
Setup
# Add Storybook to an existing Vite/Next.js project
npx storybook@latest init
# Start Storybook dev server
npm run storybook
# Build static Storybook for deployment
npm run build-storybook
Storybook 8 auto-detects your framework (Vite, Next.js, webpack) and installs the right builder. Your .storybook/ folder gets a main.ts and preview.ts.
Writing Stories: CSF3
Component Story Format 3 (CSF3) is the current standard — stories are plain objects, not functions:
// src/components/Button/Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
// Meta: configures the component in Storybook's sidebar
const meta: Meta<typeof Button> = {
title: 'UI/Button', // Sidebar path
component: Button,
tags: ['autodocs'], // Auto-generate docs page
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg'],
},
onClick: { action: 'clicked' }, // Log clicks in Actions panel
},
}
export default meta
type Story = StoryObj<typeof Button>
// Each named export = one story
export const Primary: Story = {
args: {
children: 'Click me',
variant: 'primary',
size: 'md',
},
}
export const Secondary: Story = {
args: {
children: 'Secondary',
variant: 'secondary',
size: 'md',
},
}
export const Large: Story = {
args: {
children: 'Large Button',
variant: 'primary',
size: 'lg',
},
}
export const Disabled: Story = {
args: {
children: 'Disabled',
variant: 'primary',
disabled: true,
},
}
Args and Controls
Args are the props Storybook passes to your component. The Controls panel auto-generates UI to modify them:
// argTypes control the Controls panel UI
const meta: Meta<typeof Input> = {
component: Input,
argTypes: {
type: {
control: 'select',
options: ['text', 'email', 'password', 'number'],
description: 'HTML input type',
},
label: { control: 'text' },
placeholder: { control: 'text' },
disabled: { control: 'boolean' },
errorMessage: { control: 'text', description: 'Shows error state when set' },
// Hide internal/callback props from controls
onChange: { table: { disable: true } },
ref: { table: { disable: true } },
},
}
// Story with all states — renders multiple variants at once
export const AllStates: Story = {
render: () => (
<div className="flex flex-col gap-4">
<Input label="Default" placeholder="Enter text..." />
<Input label="With value" value="hello@example.com" type="email" />
<Input label="Disabled" disabled placeholder="Can't edit" />
<Input label="Error" errorMessage="This field is required" />
</div>
),
}
Decorators and Providers
Decorators wrap every story — use them to provide context, themes, or routing:
// .storybook/preview.ts — global decorators apply to every story
import type { Preview } from '@storybook/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import '../src/index.css' // Import your global styles
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }
})
const preview: Preview = {
decorators: [
(Story) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<div className="p-6">
<Story />
</div>
</MemoryRouter>
</QueryClientProvider>
),
],
parameters: {
layout: 'centered', // 'centered' | 'fullscreen' | 'padded'
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#030712' },
{ name: 'light', value: '#ffffff' },
],
},
},
}
export default preview
// Story-level decorator — overrides global for one story
export const InDarkContainer: Story = {
decorators: [
(Story) => (
<div className="bg-gray-900 p-8 rounded-xl">
<Story />
</div>
),
],
args: { variant: 'primary' },
}
Interaction Testing
The play function lets you write user interaction tests that run inside Storybook's Interactions panel:
// Install
npm install @storybook/test @storybook/addon-interactions
// LoginForm.stories.ts
import { userEvent, within, expect } from '@storybook/test'
export const SuccessfulLogin: Story = {
args: { onLogin: fn() }, // fn() from @storybook/test — spy function
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement)
// Type into fields
await userEvent.type(canvas.getByLabelText('Email'), 'user@example.com')
await userEvent.type(canvas.getByLabelText('Password'), 'secret123')
// Click submit
await userEvent.click(canvas.getByRole('button', { name: /sign in/i }))
// Assert
await expect(args.onLogin).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'secret123',
})
},
}
export const ValidationError: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
// Submit without filling fields
await userEvent.click(canvas.getByRole('button', { name: /sign in/i }))
// Error messages should appear
await expect(canvas.getByText('Email is required')).toBeInTheDocument()
await expect(canvas.getByText('Password is required')).toBeInTheDocument()
},
}
Accessibility Testing
# Install a11y addon
npm install @storybook/addon-a11y
# .storybook/main.ts
addons: ['@storybook/addon-a11y']
// Per-story a11y config
export const IconButton: Story = {
args: { icon: '🔍', label: 'Search' },
parameters: {
a11y: {
config: {
rules: [
{ id: 'color-contrast', enabled: true },
{ id: 'button-name', enabled: true },
],
},
},
},
}
// Disable a11y check for a story (e.g., WIP)
export const Draft: Story = {
parameters: { a11y: { disable: true } },
}
Visual Regression with Chromatic
npm install --save-dev chromatic
# Run visual tests (uploads to Chromatic cloud)
npx chromatic --project-token=<your-token>
# .github/workflows/chromatic.yml
name: Chromatic
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 }}
buildScriptName: build-storybook
Auto-Generated Docs
// Add 'autodocs' tag to meta to generate a Docs page
const meta: Meta<typeof Button> = {
component: Button,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: 'Primary action button. Use `variant="danger"` for destructive actions.',
},
},
},
}
// Enrich with JSDoc — auto-shown in Props table
interface ButtonProps {
/** Visual style of the button */
variant: 'primary' | 'secondary' | 'danger'
/** Controls padding and font size */
size?: 'sm' | 'md' | 'lg'
/** Shows spinner and disables interaction */
loading?: boolean
}