Storybook: Build UI Components in Isolation
Storybook: Build UI Components in Isolation
Component-driven development is the dominant paradigm for modern UIs — but developing components inside the full application context is slow. You need the right mock data, navigate to the right screen, and toggle the right state just to see a button in its disabled state. Storybook solves this by giving each component its own development environment.
Storybook is an open-source tool for building UI components in isolation. Every component gets a "story" — a defined state or variant. Storybook renders them in a dedicated sandbox, completely decoupled from your application's routing, data fetching, and business logic.
What Storybook Does
- Component sandbox: See your component in every state without navigating your app
- Interactive controls: Toggle props, colors, and text in real time via a UI panel
- Documentation: Auto-generate docs from your stories and code comments
- Visual regression testing: Detect pixel-level changes between commits (with Chromatic)
- Accessibility testing: Integrated a11y checking with axe-core
- Multi-framework: React, Vue, Angular, Svelte, Web Components, and more
Installation
New Project
# In your existing project root
npx storybook@latest init
Storybook auto-detects your framework (React, Vue, etc.) and installs the right configuration. After setup, run:
npm run storybook
Storybook opens at http://localhost:6006.
With Vite
For projects using Vite:
npx storybook@latest init --builder vite
Or if Storybook already chose Webpack and you want to switch:
npm install --save-dev @storybook/builder-vite
Update .storybook/main.ts:
const config: StorybookConfig = {
framework: {
name: '@storybook/react-vite',
options: {},
},
};
Writing Your First Story
Stories are files co-located with your components. The convention is ComponentName.stories.tsx.
Given a Button component:
// Button.tsx
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
onClick?: () => void;
}
export const Button = ({ label, variant = 'primary', disabled, onClick }: ButtonProps) => (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
Write the story file:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'], // Enable auto-generated docs
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// Stories are named exports
export const Primary: Story = {
args: {
label: 'Click me',
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
label: 'Cancel',
variant: 'secondary',
},
};
export const Danger: Story = {
args: {
label: 'Delete',
variant: 'danger',
},
};
export const Disabled: Story = {
args: {
label: 'Unavailable',
disabled: true,
},
};
Each named export is a separate story. In the Storybook UI, you'll see a tree:
Components
└── Button
├── Primary
├── Secondary
├── Danger
└── Disabled
Controls Panel
With argTypes configured, Storybook generates a Controls panel at the bottom of the canvas. You can:
- Change
variantvia a dropdown - Toggle
disabledvia a checkbox - Edit
labelvia a text input
Changes update the component in real time — no code editing required. This is great for QA, design review, and exploring edge cases.
Auto-Generated Documentation
Add tags: ['autodocs'] to your meta object (shown above). Storybook generates a documentation page for the component with:
- Prop types table (inferred from TypeScript types)
- Description (from JSDoc comments)
- Interactive stories embedded inline
To add descriptions, use JSDoc on your props:
interface ButtonProps {
/** The button's text label */
label: string;
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'danger';
/** Prevents user interaction */
disabled?: boolean;
}
The docs page is automatically updated when you change types — no manual documentation maintenance.
Accessibility Testing
Install the a11y addon:
npm install --save-dev @storybook/addon-a11y
Add to .storybook/main.ts:
const config: StorybookConfig = {
addons: [
'@storybook/addon-a11y',
// ... other addons
],
};
Each story now has an Accessibility panel showing WCAG violations from axe-core. Issues are categorized by severity (critical, serious, moderate, minor).
You can configure per-story accessibility rules:
export const WithFocus: Story = {
parameters: {
a11y: {
// Disable specific rules for this story
config: {
rules: [{ id: 'color-contrast', enabled: false }],
},
},
},
};
Mocking Data and Imports
Complex components need mock data. Use Storybook's decorator pattern:
// ProductCard.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ProductCard } from './ProductCard';
const meta: Meta<typeof ProductCard> = {
title: 'Commerce/ProductCard',
component: ProductCard,
decorators: [
(Story) => (
<div style={{ maxWidth: '320px', margin: '0 auto' }}>
<Story />
</div>
),
],
};
export const WithProduct: Story = {
args: {
product: {
id: '1',
name: 'Wireless Headphones',
price: 79.99,
image: '/placeholder.jpg',
inStock: true,
},
},
};
export const OutOfStock: Story = {
args: {
product: {
...WithProduct.args.product,
inStock: false,
},
},
};
Mocking API Calls
Use MSW (Mock Service Worker) for component stories that make API calls:
npm install --save-dev msw msw-storybook-addon
In .storybook/preview.tsx:
import { initialize, mswLoader } from 'msw-storybook-addon';
initialize();
export const loaders = [mswLoader];
In your story:
import { http, HttpResponse } from 'msw';
export const WithLoadedData: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/products', () => {
return HttpResponse.json([
{ id: '1', name: 'Product A', price: 29.99 },
{ id: '2', name: 'Product B', price: 49.99 },
]);
}),
],
},
},
};
export const WithError: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/products', () => {
return new HttpResponse(null, { status: 500 });
}),
],
},
},
};
Visual Regression Testing with Chromatic
Chromatic is Storybook's companion service for visual regression testing. On every commit, it captures screenshots of all stories and highlights pixel differences.
npm install --save-dev chromatic
# Run locally
npx chromatic --project-token=YOUR_TOKEN
# Or in CI (GitHub Actions)
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
Chromatic shows a diff UI where reviewers approve or reject visual changes. This prevents accidental UI regressions from sneaking into production.
Interaction Testing
Test component behavior with simulated user events:
import { within, userEvent, expect } from '@storybook/test';
export const SubmitForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emailInput = canvas.getByLabelText('Email');
await userEvent.type(emailInput, '[email protected]');
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);
await expect(canvas.getByText('Success!')).toBeInTheDocument();
},
};
These play functions run automatically in the Storybook canvas and can be included in CI via:
npx storybook test
Storybook Configuration
.storybook/main.ts controls Storybook's setup:
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-essentials', // Controls, docs, actions, etc.
'@storybook/addon-a11y',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
.storybook/preview.tsx sets global decorators and parameters:
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css'; // Import global styles
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
layout: 'centered',
},
decorators: [
// Wrap all stories with providers
(Story) => (
<ThemeProvider theme="light">
<Story />
</ThemeProvider>
),
],
};
Deploying Storybook
Build a static version for sharing:
npm run build-storybook
This outputs to storybook-static/. Deploy it anywhere static files are served:
# GitHub Pages
npx gh-pages -d storybook-static
# Netlify
# Drag and drop storybook-static/ to netlify.com
# Vercel
vercel storybook-static/
Teams often deploy Storybook to a subdomain (e.g., storybook.yourdomain.com) so designers and PMs can review component states without running the dev environment locally.
Storybook vs Alternatives
| Tool | Focus | Best For |
|---|---|---|
| Storybook | Component isolation + docs | Large component libraries |
| Histoire | Vue/Svelte-first | Vue-heavy projects |
| Ladle | Minimal Storybook alt | Simple React projects |
| Sandpack | In-browser sandbox | Educational/docs sites |
Storybook dominates for React projects at scale. Histoire is the go-to for Vue projects. Ladle is worth considering if Storybook feels heavy for a small project.
Wrapping Up
Storybook shifts the unit of UI development from pages to components. Once set up, you can build and review every component state without spinning up your full application, which dramatically speeds up UI development and review cycles.
The initial setup investment — stories, mocks, addons — pays off quickly for component libraries or design systems. For application development, even a few stories for your most complex components can save hours of debugging.
Frontend tooling guides like this land in the DevTools Guide newsletter every week — subscribe to stay sharp.