Storybook Setup & Stories
Storybook is an isolated UI development environment. Stories are the unit of work — each story renders a component in a specific state. Use CSF (Component Story Format) for interoperable, testable stories.
Installation & Configuration
# Initialize Storybook in an existing project
npx storybook@latest init
# Detects framework automatically (React, Vue, Svelte, Angular, etc.)
# Creates .storybook/main.ts and .storybook/preview.ts
# Adds example stories in src/stories/
# Dev server
npx storybook dev -p 6006
# Production static build
npx storybook build
npx http-server storybook-static # serve locally
# Key addons
npm install --save-dev
@storybook/addon-essentials # controls, actions, docs, viewport, backgrounds
@storybook/addon-a11y # accessibility auditing
@storybook/addon-interactions # interaction testing
@storybook/test # play functions
msw-storybook-addon # API mocking with MSW
storybook-addon-themes # theme switching.storybook/main.ts — Framework Config
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
// Story file patterns — where to find stories
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@chromatic-com/storybook', // visual regression testing
],
framework: {
name: '@storybook/react-vite', // use Vite builder
options: {},
},
// Expose env variables to stories
env: (config) => ({
...config,
STORYBOOK_API_URL: process.env.STORYBOOK_API_URL ?? 'http://localhost:3000',
}),
// Static files served at /public
staticDirs: ['../public'],
// TypeScript config
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => !prop.parent?.fileName.includes('node_modules'),
},
},
};
export default config;Writing Stories — CSF Format
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
// Meta — component-level config
const meta: Meta<typeof Button> = {
title: 'UI/Button', // sidebar group: UI > Button
component: Button,
tags: ['autodocs'], // auto-generate docs page from JSDoc + props
parameters: {
layout: 'centered', // 'centered' | 'fullscreen' | 'padded'
docs: {
description: {
component: 'Primary UI button. Use for main actions.',
},
},
},
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger', 'ghost'],
description: 'Visual style',
table: { defaultValue: { summary: 'primary' } },
},
size: {
control: { type: 'radio' },
options: ['sm', 'md', 'lg'],
},
disabled: { control: 'boolean' },
isLoading: { control: 'boolean' },
onClick: { action: 'clicked' }, // log to Actions panel
},
args: {
// Default args for all stories in this file
onClick: fn(), // spy function (Storybook 8+)
variant: 'primary',
size: 'md',
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// Stories
export const Primary: Story = {
args: { children: 'Click me' },
};
export const Danger: Story = {
args: { children: 'Delete', variant: 'danger' },
};
export const Disabled: Story = {
args: { children: 'Disabled', disabled: true },
};
export const Loading: Story = {
args: { children: 'Saving...', isLoading: true },
};
// Custom render function
export const AllVariants: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{(['primary', 'secondary', 'danger', 'ghost'] as const).map((v) => (
<Button key={v} {...args} variant={v}>{v}</Button>
))}
</div>
),
};Global Decorators & Preview Config
// .storybook/preview.tsx — global config applied to all stories
import type { Preview } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from '../src/providers/ThemeProvider';
import '../src/styles/globals.css';
const preview: Preview = {
decorators: [
// Wrap all stories in required providers
(Story) => (
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<BrowserRouter>
<ThemeProvider defaultTheme="light">
<div style={{ fontFamily: 'Inter, sans-serif', padding: '1rem' }}>
<Story />
</div>
</ThemeProvider>
</BrowserRouter>
</QueryClientProvider>
),
],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#0f172a' },
{ name: 'gray', value: '#f1f5f9' },
],
},
viewport: {
viewports: {
mobile: { name: 'Mobile', styles: { width: '375px', height: '812px' } },
tablet: { name: 'Tablet', styles: { width: '768px', height: '1024px' } },
desktop: { name: 'Desktop', styles: { width: '1440px', height: '900px' } },
},
},
},
};
export default preview;Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free