Storybook is a powerful tool for building UI components in isolation and documenting them. This guide covers setup, configuration, and best practices for using Storybook effectively.
# React
npx storybook@latest init
# Vue
npx storybook@latest init --builder vue3
# Angular
npx storybook@latest init --builder angular
# Manual installation
npm install --save-dev @storybook/react @storybook/addon-essentials// .storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-performance',
'@storybook/addon-viewport',
'@storybook/addon-docs',
],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-webpack5',
},
features: {
buildStoriesJson: true,
},
};// .storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-performance',
'@storybook/addon-viewport',
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-actions',
'@storybook/addon-backgrounds',
'@storybook/addon-measure',
'@storybook/addon-outline',
],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-webpack5',
},
webpackFinal: async (config) => {
// Add support for CSS modules
config.module.rules.push({
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
],
});
return config;
},
features: {
buildStoriesJson: true,
},
};// .storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-docs',
],
framework: '@storybook/react',
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
};// Button.stories.js
import { Button } from './Button';
export default {
title: 'Components/Button',
component: Button,
parameters: {
docs: {
description: {
component: 'A reusable button component with multiple variants and sizes.',
},
},
},
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'danger'],
description: 'The visual style variant of the button',
},
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
description: 'The size of the button',
},
disabled: {
control: { type: 'boolean' },
description: 'Whether the button is disabled',
},
onClick: {
action: 'clicked',
description: 'Function called when button is clicked',
},
},
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
variant: 'primary',
children: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
variant: 'secondary',
children: 'Button',
};
export const Danger = Template.bind({});
Danger.args = {
variant: 'danger',
children: 'Button',
};
export const Large = Template.bind({});
Large.args = {
size: 'large',
children: 'Large Button',
};
export const Small = Template.bind({});
Small.args = {
size: 'small',
children: 'Small Button',
};// Card.stories.js
import { Card } from './Card';
import { Button } from '../Button/Button';
export default {
title: 'Components/Card',
component: Card,
decorators: [
(Story) => (
<div style={{ padding: '20px', backgroundColor: '#f5f5f5' }}>
<Story />
</div>
),
],
parameters: {
layout: 'centered',
},
};
const Template = (args) => <Card {...args} />;
export const Default = Template.bind({});
Default.args = {
title: 'Card Title',
children: 'This is the card content.',
};
export const WithActions = Template.bind({});
WithActions.args = {
title: 'Card with Actions',
children: 'This card has action buttons.',
actions: (
<div style={{ display: 'flex', gap: '8px' }}>
<Button size="small">Cancel</Button>
<Button size="small" variant="primary">Save</Button>
</div>
),
};
export const Loading = Template.bind({});
Loading.args = {
title: 'Loading Card',
children: 'This card is in a loading state.',
loading: true,
};
export const Error = Template.bind({});
Error.args = {
title: 'Error Card',
children: 'This card shows an error state.',
error: 'Something went wrong',
};// Counter.stories.js
import { useState } from 'react';
import { Counter } from './Counter';
export default {
title: 'Components/Counter',
component: Counter,
};
const Template = (args) => {
const [count, setCount] = useState(args.initialValue || 0);
return (
<Counter
{...args}
value={count}
onChange={setCount}
/>
);
};
export const Default = Template.bind({});
Default.args = {
initialValue: 0,
min: 0,
max: 100,
};
export const WithStep = Template.bind({});
WithStep.args = {
initialValue: 10,
step: 5,
min: 0,
max: 50,
};// .storybook/main.js
module.exports = {
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-performance',
'@storybook/addon-viewport',
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-actions',
'@storybook/addon-backgrounds',
'@storybook/addon-measure',
'@storybook/addon-outline',
],
};// .storybook/addons/theme-addon.js
import { addons } from '@storybook/addons';
import { STORY_CHANGED } from '@storybook/core-events';
const themeAddon = {
id: 'theme-addon',
title: 'Theme',
type: 'tool',
match: ({ viewMode }) => viewMode === 'story',
render: () => {
const channel = addons.getChannel();
const handleThemeChange = (theme) => {
channel.emit('theme-changed', theme);
};
return {
type: 'div',
innerHTML: `
<div style="padding: 10px;">
<label>Theme:</label>
<select onchange="window.handleThemeChange(this.value)">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
`,
};
},
};
addons.register('theme-addon', () => {
addons.add('theme-addon', themeAddon);
});// .storybook/main.js
module.exports = {
addons: [
'@storybook/addon-a11y',
],
};
// Button.stories.js
export default {
title: 'Components/Button',
component: Button,
parameters: {
a11y: {
element: '#root',
config: {
rules: [
{
id: 'color-contrast',
enabled: true,
},
],
},
},
},
};<!-- Button.mdx -->
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { Button } from './Button';
<Meta title="Components/Button" component={Button} />
# Button
A reusable button component with multiple variants and sizes.
## Usage
```jsx
import { Button } from './Button';
<Button variant="primary" size="large">
Click me
</Button>- Use primary buttons for the main action on a page
- Use secondary buttons for secondary actions
- Use danger buttons for destructive actions
- Ensure buttons have sufficient contrast for accessibility
### Component Documentation
```javascript
// Button.stories.js
export default {
title: 'Components/Button',
component: Button,
parameters: {
docs: {
description: {
component: `
A reusable button component with multiple variants and sizes.
## Usage
\`\`\`jsx
import { Button } from './Button';
<Button variant="primary" size="large">
Click me
</Button>
\`\`\`
## Design Guidelines
- Use primary buttons for the main action on a page
- Use secondary buttons for secondary actions
- Use danger buttons for destructive actions
- Ensure buttons have sufficient contrast for accessibility
`,
},
},
},
};
// .storybook/main.js
module.exports = {
addons: [
'@storybook/addon-storyshots',
'@storybook/addon-storyshots-puppeteer',
],
};
// Button.stories.js
export default {
title: 'Components/Button',
component: Button,
parameters: {
storyshots: {
disable: false,
},
},
};// Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByText('Click me')).toBeDisabled();
});
});// Button.stories.js
export default {
title: 'Components/Button',
component: Button,
parameters: {
a11y: {
config: {
rules: [
{
id: 'color-contrast',
enabled: true,
},
{
id: 'button-name',
enabled: true,
},
],
},
},
},
};// .storybook/design-tokens.js
export const tokens = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
typography: {
fontFamily: 'Inter, sans-serif',
fontSize: {
sm: '14px',
md: '16px',
lg: '18px',
xl: '24px',
},
},
};
// Button.stories.js
import { tokens } from '../.storybook/design-tokens';
export default {
title: 'Components/Button',
component: Button,
parameters: {
docs: {
description: {
component: `
Button component using design tokens:
- Primary color: ${tokens.colors.primary}
- Medium spacing: ${tokens.spacing.md}
- Medium font size: ${tokens.typography.fontSize.md}
`,
},
},
},
};// .storybook/theme.js
import { createTheme } from '@mui/material/styles';
export const lightTheme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#007bff',
},
},
});
export const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#90caf9',
},
},
});
// .storybook/preview.js
import { lightTheme, darkTheme } from './theme';
export const parameters = {
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: lightTheme.palette.background.default },
{ name: 'dark', value: darkTheme.palette.background.default },
],
},
};// ❌ Problem: Story not rendering
export const MyStory = () => <Button>Click me</Button>;
// ✅ Solution: Use Template pattern
const Template = (args) => <Button {...args} />;
export const MyStory = Template.bind({});
MyStory.args = {
children: 'Click me',
};// ❌ Problem: Controls not showing
export default {
title: 'Components/Button',
component: Button,
// Missing argTypes
};
// ✅ Solution: Add argTypes
export default {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'danger'],
},
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
},
},
};// ❌ Problem: TypeScript errors in stories
export const MyStory = (args: ButtonProps) => <Button {...args} />;
// ✅ Solution: Use proper typing
import { ComponentStory, ComponentMeta } from '@storybook/react';
const meta: ComponentMeta<typeof Button> = {
title: 'Components/Button',
component: Button,
};
export default meta;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const MyStory = Template.bind({});
MyStory.args = {
children: 'Click me',
};