Storybook Cheatsheet¶
Storybook - Build UIs in Isolation
Storybook is a frontend workshop for building UI components and pages in isolation. It helps you develop and share hard-to-reach states and edge cases without needing to run your whole app. Thousands of teams use it for UI development, testing, and documentation.
Table of Contents¶
- Installation
- Getting Started
- Writing Stories
- Story Formats
- Controls & Actions
- Addons
- Configuration
- Theming
- Testing
- Documentation
- Deployment
- Advanced Features
- Performance
- Integration
- Troubleshooting
- Best Practices
Installation¶
Quick Start¶
# Initialize Storybook in existing project
npx storybook@latest init
# Create new project with Storybook
npx create-react-app my-app
cd my-app
npx storybook@latest init
# Start Storybook
npm run storybook
Manual Installation¶
# Install Storybook CLI
npm install -g @storybook/cli
# Initialize in project
sb init
# Or specify framework
sb init --type react
sb init --type vue3
sb init --type angular
Framework-Specific Setup¶
# React
npx storybook@latest init --type react
# Vue 3
npx storybook@latest init --type vue3
# Angular
npx storybook@latest init --type angular
# Svelte
npx storybook@latest init --type svelte
# Web Components
npx storybook@latest init --type web-components
# HTML
npx storybook@latest init --type html
Project Structure¶
my-app/
├── .storybook/
│ ├── main.js # Main configuration
│ └── preview.js # Global settings
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.js
│ │ │ ├── Button.css
│ │ │ └── Button.stories.js
│ │ └── Header/
│ │ ├── Header.js
│ │ └── Header.stories.js
│ └── stories/ # Example stories
├── package.json
└── README.md
Getting Started¶
Basic Story¶
// src/components/Button/Button.stories.js
import { Button } from './Button';
export default {
title: 'Example/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
backgroundColor: { control: 'color' },
},
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};
Component Example¶
// src/components/Button/Button.js
import React from 'react';
import PropTypes from 'prop-types';
import './button.css';
export const Button = ({ primary = false, backgroundColor = null, size = 'medium', label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={{ backgroundColor }}
{...props}
>
{label}
</button>
);
};
Button.propTypes = {
primary: PropTypes.bool,
backgroundColor: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
Button.defaultProps = {
primary: false,
onClick: undefined,
};
Running Storybook¶
# Start development server
npm run storybook
# Build static Storybook
npm run build-storybook
# Serve built Storybook
npx http-server storybook-static
# Start with specific port
npm run storybook -- --port 9001
# Start in quiet mode
npm run storybook -- --quiet
Writing Stories¶
Story Structure¶
// Component.stories.js
import { Component } from './Component';
// Default export - story metadata
export default {
title: 'Path/To/Component',
component: Component,
parameters: {
// Story-level parameters
},
argTypes: {
// Control definitions
},
args: {
// Default args for all stories
},
};
// Named exports - individual stories
export const Default = {};
export const WithProps = {
args: {
prop1: 'value1',
prop2: 'value2',
},
};
export const CustomStory = {
render: (args) => <Component {...args} />,
args: {
// Story-specific args
},
};
Story Naming¶
// Organize stories with hierarchy
export default {
title: 'Design System/Components/Button',
// Creates: Design System > Components > Button
};
export default {
title: 'Pages/Dashboard/UserProfile',
// Creates: Pages > Dashboard > UserProfile
};
export default {
title: 'Example/Button',
// Creates: Example > Button
};
Multiple Components¶
// stories/Forms.stories.js
import { Input } from '../components/Input';
import { Button } from '../components/Button';
import { Form } from '../components/Form';
export default {
title: 'Forms/LoginForm',
};
export const LoginForm = () => (
<Form>
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Button primary>Login</Button>
</Form>
);
export const SignupForm = () => (
<Form>
<Input placeholder="Full Name" />
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Input type="password" placeholder="Confirm Password" />
<Button primary>Sign Up</Button>
</Form>
);
Story Parameters¶
export default {
title: 'Example/Button',
component: Button,
parameters: {
// Layout
layout: 'centered', // 'centered' | 'fullscreen' | 'padded'
// Backgrounds
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
],
},
// Viewport
viewport: {
defaultViewport: 'mobile1',
},
// Documentation
docs: {
description: {
component: 'Button component for user interactions',
},
},
},
};
Story Formats¶
CSF 3.0 (Current)¶
// Modern format with object syntax
export default {
title: 'Example/Button',
component: Button,
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
primary: false,
label: 'Button',
},
};
CSF 2.0 (Legacy)¶
// Function-based format
export default {
title: 'Example/Button',
component: Button,
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
primary: false,
label: 'Button',
};
Custom Render Functions¶
export const CustomRender = {
render: (args) => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Count: {count}</p>
<Button
{...args}
onClick={() => setCount(count + 1)}
/>
</div>
);
},
args: {
label: 'Increment',
},
};
Play Functions¶
import { userEvent, within } from '@storybook/testing-library';
export const InteractiveExample = {
args: {
label: 'Click me',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
// Simulate user interaction
await userEvent.click(button);
// Add assertions if needed
// expect(button).toHaveClass('clicked');
},
};
Controls & Actions¶
ArgTypes Configuration¶
export default {
title: 'Example/Button',
component: Button,
argTypes: {
// Text input
label: {
control: 'text',
description: 'Button label text',
},
// Boolean
primary: {
control: 'boolean',
description: 'Primary button style',
},
// Select dropdown
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Button size',
},
// Radio buttons
variant: {
control: 'radio',
options: ['primary', 'secondary', 'danger'],
},
// Color picker
backgroundColor: {
control: 'color',
},
// Number input
borderRadius: {
control: { type: 'number', min: 0, max: 50, step: 1 },
},
// Range slider
opacity: {
control: { type: 'range', min: 0, max: 1, step: 0.1 },
},
// Object editor
style: {
control: 'object',
},
// Array editor
items: {
control: 'object',
},
// Date picker
createdAt: {
control: 'date',
},
// Disable control
onClick: {
action: 'clicked',
control: false,
},
},
};
Actions¶
import { action } from '@storybook/addon-actions';
export default {
title: 'Example/Button',
component: Button,
argTypes: {
onClick: { action: 'clicked' },
onMouseEnter: { action: 'mouse-enter' },
onMouseLeave: { action: 'mouse-leave' },
},
};
// Or in individual stories
export const WithActions = {
args: {
label: 'Button',
onClick: action('button-click'),
onMouseEnter: action('mouse-enter'),
},
};
// Multiple actions
export const MultipleActions = {
args: {
label: 'Multi Action Button',
onClick: (...args) => {
action('primary-click')(...args);
action('analytics-track')('button-clicked');
},
},
};
Advanced Controls¶
export default {
title: 'Example/Form',
component: Form,
argTypes: {
// Conditional controls
showAdvanced: {
control: 'boolean',
},
advancedOptions: {
control: 'object',
if: { arg: 'showAdvanced', truthy: true },
},
// Custom control
theme: {
control: {
type: 'select',
labels: {
light: 'Light Theme',
dark: 'Dark Theme',
auto: 'Auto (System)',
},
},
options: ['light', 'dark', 'auto'],
},
// Multi-select
features: {
control: 'multi-select',
options: ['feature1', 'feature2', 'feature3'],
},
},
};
Addons¶
Essential Addons¶
// .storybook/main.js
module.exports = {
addons: [
'@storybook/addon-essentials', // Includes most common addons
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-design-tokens',
],
};
// Individual addons (if not using essentials)
module.exports = {
addons: [
'@storybook/addon-controls',
'@storybook/addon-actions',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
'@storybook/addon-docs',
'@storybook/addon-toolbars',
],
};
Accessibility Addon¶
// .storybook/preview.js
export const parameters = {
a11y: {
// Optional selector to inspect
element: '#storybook-root',
config: {
rules: [
{
// Disable specific rules
id: 'color-contrast',
enabled: false,
},
],
},
// Show violations in action panel
manual: true,
},
};
// In stories
export const AccessibleButton = {
args: {
label: 'Accessible Button',
},
parameters: {
a11y: {
// Story-specific a11y config
config: {
rules: [
{ id: 'button-name', enabled: true },
],
},
},
},
};
Viewport Addon¶
// .storybook/preview.js
export const parameters = {
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: {
width: '375px',
height: '667px',
},
},
tablet: {
name: 'Tablet',
styles: {
width: '768px',
height: '1024px',
},
},
desktop: {
name: 'Desktop',
styles: {
width: '1024px',
height: '768px',
},
},
},
defaultViewport: 'mobile',
},
};
Design Tokens Addon¶
// design-tokens.json
{
"colors": {
"primary": "#007bff",
"secondary": "#6c757d",
"success": "#28a745",
"danger": "#dc3545"
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px"
},
"typography": {
"fontFamily": {
"primary": "Inter, sans-serif",
"mono": "Monaco, monospace"
},
"fontSize": {
"sm": "14px",
"md": "16px",
"lg": "18px",
"xl": "24px"
}
}
}
// .storybook/main.js
module.exports = {
addons: [
{
name: '@storybook/addon-design-tokens',
options: {
designTokenGlob: '**/design-tokens.json',
},
},
],
};
Custom Addons¶
// .storybook/addons/theme-switcher/register.js
import { addons, types } from '@storybook/addons';
import { ADDON_ID, TOOL_ID } from './constants';
import { Tool } from './Tool';
addons.register(ADDON_ID, () => {
addons.add(TOOL_ID, {
type: types.TOOL,
title: 'Theme Switcher',
render: Tool,
});
});
// .storybook/addons/theme-switcher/Tool.js
import React from 'react';
import { useGlobals } from '@storybook/api';
import { IconButton } from '@storybook/components';
export const Tool = () => {
const [globals, updateGlobals] = useGlobals();
const theme = globals.theme || 'light';
const toggleTheme = () => {
updateGlobals({
theme: theme === 'light' ? 'dark' : 'light',
});
};
return (
<IconButton
key="theme-switcher"
title="Toggle theme"
onClick={toggleTheme}
>
{theme === 'light' ? '🌙' : '☀️'}
</IconButton>
);
};
Configuration¶
Main Configuration¶
// .storybook/main.js
module.exports = {
// Stories location
stories: [
'../src/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)',
],
// Addons
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
// Framework
framework: {
name: '@storybook/react-vite',
options: {},
},
// Features
features: {
buildStoriesJson: true,
storyStoreV7: true,
},
// TypeScript
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
// Webpack customization
webpackFinal: async (config) => {
// Add custom webpack config
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
});
return config;
},
// Vite customization
viteFinal: async (config) => {
// Customize Vite config
config.define = {
...config.define,
global: 'globalThis',
};
return config;
},
// Documentation
docs: {
autodocs: 'tag',
},
// Static directories
staticDirs: ['../public'],
};
Preview Configuration¶
// .storybook/preview.js
import '../src/index.css'; // Global styles
export const parameters = {
// Actions
actions: { argTypesRegex: '^on[A-Z].*' },
// Controls
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
expanded: true,
sort: 'requiredFirst',
},
// Layout
layout: 'centered',
// Backgrounds
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
{ name: 'gray', value: '#f8f9fa' },
],
},
// Viewport
viewport: {
viewports: {
mobile1: {
name: 'Small mobile',
styles: { width: '320px', height: '568px' },
},
mobile2: {
name: 'Large mobile',
styles: { width: '414px', height: '896px' },
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
},
},
},
// Documentation
docs: {
theme: themes.light,
source: {
state: 'open',
},
},
};
// Global decorators
export const decorators = [
(Story) => (
<div style={{ margin: '3em', fontFamily: 'Inter, sans-serif' }}>
<Story />
</div>
),
];
// Global args
export const args = {
// Default args for all stories
};
// Global arg types
export const argTypes = {
// Global arg types
};
Environment Variables¶
// .storybook/main.js
module.exports = {
env: (config) => ({
...config,
API_URL: 'https://api.example.com',
FEATURE_FLAG: true,
}),
webpackFinal: async (config) => {
config.plugins.push(
new webpack.DefinePlugin({
'process.env.STORYBOOK': JSON.stringify(true),
})
);
return config;
},
};
// In components
const apiUrl = process.env.STORYBOOK
? 'https://api-mock.example.com'
: process.env.API_URL;
Theming¶
Custom Theme¶
// .storybook/theme.js
import { create } from '@storybook/theming';
export default create({
base: 'light', // 'light' | 'dark'
// Brand
brandTitle: 'My Company Storybook',
brandUrl: 'https://example.com',
brandImage: 'https://example.com/logo.svg',
brandTarget: '_self',
// Colors
colorPrimary: '#FF4785',
colorSecondary: '#029CFD',
// UI
appBg: '#F6F9FC',
appContentBg: '#FFFFFF',
appBorderColor: '#E6ECF0',
appBorderRadius: 4,
// Typography
fontBase: '"Inter", sans-serif',
fontCode: 'Monaco, monospace',
// Text colors
textColor: '#2E3438',
textInverseColor: '#FFFFFF',
textMutedColor: '#798186',
// Toolbar
barTextColor: '#798186',
barSelectedColor: '#029CFD',
barBg: '#FFFFFF',
// Form
inputBg: '#FFFFFF',
inputBorder: '#E6ECF0',
inputTextColor: '#2E3438',
inputBorderRadius: 4,
});
// .storybook/manager.js
import { addons } from '@storybook/addons';
import theme from './theme';
addons.setConfig({
theme,
});
Dark Theme¶
// .storybook/dark-theme.js
import { create } from '@storybook/theming';
export default create({
base: 'dark',
brandTitle: 'My Company Storybook',
brandUrl: 'https://example.com',
brandImage: 'https://example.com/logo-white.svg',
colorPrimary: '#FF4785',
colorSecondary: '#029CFD',
appBg: '#1A1A1A',
appContentBg: '#2D2D2D',
appBorderColor: '#404040',
textColor: '#FFFFFF',
textInverseColor: '#1A1A1A',
textMutedColor: '#CCCCCC',
barTextColor: '#CCCCCC',
barSelectedColor: '#029CFD',
barBg: '#2D2D2D',
inputBg: '#404040',
inputBorder: '#666666',
inputTextColor: '#FFFFFF',
});
Theme Switching¶
// .storybook/preview.js
import { themes } from '@storybook/theming';
export const parameters = {
docs: {
theme: themes.light,
},
darkMode: {
// Override the default dark theme
dark: { ...themes.dark, appBg: 'black' },
// Override the default light theme
light: { ...themes.normal, appBg: 'white' },
// Set the initial theme
current: 'light',
// Disable the addon for specific stories
stylePreview: true,
},
};
// Global decorator for theme switching
export const decorators = [
(Story, context) => {
const theme = context.globals.theme || 'light';
return (
<div className={`theme-${theme}`}>
<Story />
</div>
);
},
];
// Global types for theme switcher
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: [
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
],
showName: true,
},
},
};
Testing¶
Visual Testing¶
// .storybook/test-runner.js
const { getStoryContext } = require('@storybook/test-runner');
module.exports = {
async postRender(page, context) {
const storyContext = await getStoryContext(page, context);
// Skip visual tests for specific stories
if (storyContext.parameters?.skipVisualTest) {
return;
}
// Take screenshot
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotIdentifier: context.id,
});
},
};
// In stories
export const VisualTest = {
args: {
label: 'Visual Test Button',
},
parameters: {
// Skip this story in visual tests
skipVisualTest: true,
},
};
Interaction Testing¶
// Button.stories.js
import { userEvent, within, expect } from '@storybook/test';
export const InteractionTest = {
args: {
label: 'Click me',
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Find elements
const button = canvas.getByRole('button', { name: /click me/i });
// Simulate interactions
await userEvent.click(button);
// Assertions
await expect(args.onClick).toHaveBeenCalled();
// Check DOM changes
await expect(button).toHaveClass('clicked');
// Wait for async operations
await canvas.findByText('Success!');
},
};
export const FormInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Fill form
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/password/i);
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.type(emailInput, 'user@example.com');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(submitButton);
// Check results
await expect(canvas.getByText(/success/i)).toBeInTheDocument();
},
};
Unit Testing with Stories¶
// Button.test.js
import { render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';
const { Primary, Secondary, Large } = composeStories(stories);
describe('Button', () => {
test('renders primary button', () => {
render(<Primary />);
expect(screen.getByRole('button')).toHaveClass('storybook-button--primary');
});
test('renders secondary button', () => {
render(<Secondary />);
expect(screen.getByRole('button')).not.toHaveClass('storybook-button--primary');
});
test('renders large button', () => {
render(<Large />);
expect(screen.getByRole('button')).toHaveClass('storybook-button--large');
});
});
Accessibility Testing¶
// a11y.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';
expect.extend(toHaveNoViolations);
const { Primary, Secondary } = composeStories(stories);
describe('Button Accessibility', () => {
test('Primary button should not have accessibility violations', async () => {
const { container } = render(<Primary />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('Secondary button should not have accessibility violations', async () => {
const { container } = render(<Secondary />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Documentation¶
Auto Documentation¶
// Component with JSDoc
/**
* Primary UI component for user interaction
*/
export const Button = ({
/**
* Is this the principal call to action on the page?
*/
primary = false,
/**
* What background color to use
*/
backgroundColor,
/**
* How large should the button be?
*/
size = 'medium',
/**
* Button contents
*/
label,
/**
* Optional click handler
*/
onClick,
...props
}) => {
// Component implementation
};
// Stories with auto-generated docs
export default {
title: 'Example/Button',
component: Button,
tags: ['autodocs'], // Enable auto-documentation
parameters: {
docs: {
description: {
component: 'Button component for user interactions. Supports multiple sizes and variants.',
},
},
},
};
Custom Documentation¶
// Button.stories.js
export default {
title: 'Example/Button',
component: Button,
parameters: {
docs: {
description: {
component: `
# Button Component
The Button component is a fundamental UI element used throughout the application.
## Usage
\`\`\`jsx
import { Button } from './Button';
<Button primary onClick={handleClick}>
Click me
</Button>
\`\`\`
## Design Guidelines
- Use primary buttons for main actions
- Use secondary buttons for supporting actions
- Ensure sufficient color contrast for accessibility
`,
},
},
},
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
parameters: {
docs: {
description: {
story: 'Primary buttons are used for the main call-to-action on a page.',
},
},
},
};
MDX Documentation¶
<!-- Button.stories.mdx -->
import { Meta, Story, Canvas, ArgsTable, Description } from '@storybook/addon-docs';
import { Button } from './Button';
<Meta title="Example/Button" component={Button} />
# Button
<Description of={Button} />
## Examples
### Primary Button
<Canvas>
<Story name="Primary" args={{ primary: true, label: 'Button' }}>
{(args) => <Button {...args} />}
</Story>
</Canvas>
### Secondary Button
<Canvas>
<Story name="Secondary" args={{ label: 'Button' }}>
{(args) => <Button {...args} />}
</Story>
</Canvas>
## Props
<ArgsTable of={Button} />
## Usage Guidelines
- Use primary buttons sparingly, typically one per page
- Secondary buttons can be used multiple times
- Always provide meaningful labels
- Consider accessibility when choosing colors
## Code Example
```jsx
import { Button } from './Button';
function MyComponent() {
return (
<div>
<Button primary onClick={() => alert('Primary clicked!')}>
Primary Action
</Button>
<Button onClick={() => alert('Secondary clicked!')}>
Secondary Action
</Button>
</div>
);
}
### Documentation Pages
```javascript
// .storybook/main.js
module.exports = {
stories: [
'../src/**/*.stories.@(js|jsx|ts|tsx)',
'../docs/**/*.stories.mdx', // Documentation stories
],
};
// docs/Introduction.stories.mdx
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Design System
Welcome to our design system documentation.
## Getting Started
This Storybook contains all the components, patterns, and guidelines for our design system.
### Installation
```bash
npm install @company/design-system
Usage¶
Principles¶
- Consistency - Maintain visual and functional consistency
- Accessibility - Ensure all components are accessible
- Performance - Optimize for speed and efficiency
- Flexibility - Support customization and theming
Netlify Deployment¶
# netlify.toml
[build]
command = "npm run build-storybook"
publish = "storybook-static"
[build.environment]
NODE_VERSION = "16"
# Deploy
npm install -g netlify-cli
netlify deploy --prod --dir=storybook-static
Vercel Deployment¶
// vercel.json
{
"buildCommand": "npm run build-storybook",
"outputDirectory": "storybook-static",
"framework": null
}
GitHub Pages¶
# .github/workflows/storybook.yml
name: Build and Deploy Storybook
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Storybook
run: npm run build-storybook
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-static
Docker Deployment¶
# Dockerfile
FROM node:16-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build-storybook
FROM nginx:alpine
COPY --from=builder /app/storybook-static /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
AWS S3 + CloudFront¶
# Build and deploy to S3
npm run build-storybook
aws s3 sync storybook-static/ s3://my-storybook-bucket --delete
aws cloudfront create-invalidation --distribution-id ABCD1234 --paths "/*"
Advanced Features¶
Custom Webpack Configuration¶
// .storybook/main.js
const path = require('path');
module.exports = {
webpackFinal: async (config) => {
// Add alias
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../src'),
'@components': path.resolve(__dirname, '../src/components'),
};
// Add custom loader
config.module.rules.push({
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
],
});
// Add plugin
config.plugins.push(
new webpack.DefinePlugin({
__VERSION__: JSON.stringify(process.env.npm_package_version),
})
);
return config;
},
};
Custom Babel Configuration¶
// .storybook/main.js
module.exports = {
babel: async (options) => ({
...options,
plugins: [
...options.plugins,
['babel-plugin-styled-components', { displayName: true }],
],
}),
};
Environment-Specific Configuration¶
// .storybook/main.js
const isDevelopment = process.env.NODE_ENV === 'development';
module.exports = {
addons: [
'@storybook/addon-essentials',
...(isDevelopment ? ['@storybook/addon-a11y'] : []),
],
features: {
storyStoreV7: !isDevelopment, // Disable in development for faster builds
},
webpackFinal: async (config) => {
if (isDevelopment) {
// Development-specific webpack config
config.optimization.minimize = false;
}
return config;
},
};
Custom Manager UI¶
// .storybook/manager.js
import { addons } from '@storybook/addons';
addons.setConfig({
// Show/hide panels
showPanel: true,
panelPosition: 'bottom', // 'bottom' | 'right'
// Sidebar
showNav: true,
showToolbar: true,
// Initial active panel
selectedPanel: 'storybook/controls/panel',
// Sidebar tree expansion
initialActive: 'sidebar',
sidebar: {
showRoots: false,
collapsedRoots: ['other'],
},
// Toolbar
toolbar: {
title: { hidden: false },
zoom: { hidden: false },
eject: { hidden: false },
copy: { hidden: false },
fullscreen: { hidden: false },
},
});
Performance¶
Lazy Loading¶
// .storybook/main.js
module.exports = {
features: {
storyStoreV7: true, // Enable lazy loading
},
stories: [
{
directory: '../src/components',
files: '**/*.stories.*',
titlePrefix: 'Components',
},
{
directory: '../src/pages',
files: '**/*.stories.*',
titlePrefix: 'Pages',
},
],
};
Bundle Optimization¶
// .storybook/main.js
module.exports = {
webpackFinal: async (config) => {
// Code splitting
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
};
// Tree shaking
config.optimization.usedExports = true;
config.optimization.sideEffects = false;
return config;
},
};
Memory Optimization¶
// .storybook/preview.js
export const parameters = {
// Reduce memory usage
options: {
storySort: {
method: 'alphabetical',
order: ['Introduction', 'Components', 'Pages'],
locales: 'en-US',
},
},
// Disable source code addon for better performance
docs: {
source: {
state: 'closed',
},
},
};
// Cleanup decorators
export const decorators = [
(Story, context) => {
// Cleanup on story change
React.useEffect(() => {
return () => {
// Cleanup logic
};
}, [context.id]);
return <Story />;
},
];
Integration¶
Design Tools Integration¶
// Figma integration
export default {
title: 'Example/Button',
component: Button,
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/ABC123/Design-System?node-id=123%3A456',
},
},
};
// Sketch integration
export const SketchExample = {
parameters: {
design: {
type: 'sketch',
url: 'https://sketch.cloud/s/abc123',
},
},
};
// Adobe XD integration
export const XDExample = {
parameters: {
design: {
type: 'adobe-xd',
url: 'https://xd.adobe.com/view/abc123',
},
},
};
Testing Integration¶
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
testPathIgnorePatterns: ['/node_modules/', '/storybook-static/'],
// Test stories
testMatch: [
'<rootDir>/src/**/__tests__/**/*.(js|jsx|ts|tsx)',
'<rootDir>/src/**/?(*.)(spec|test).(js|jsx|ts|tsx)',
'<rootDir>/src/**/*.stories.test.(js|jsx|ts|tsx)',
],
};
// Chromatic integration
// .github/workflows/chromatic.yml
name: 'Chromatic'
on: push
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: yarn
- uses: chromaui/action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
CI/CD Integration¶
# .github/workflows/storybook-tests.yml
name: Storybook Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- run: npm ci
- run: npm run build-storybook --quiet
- run: npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on http://127.0.0.1:6006 && npm run test-storybook"
Troubleshooting¶
Common Issues¶
# Port already in use
npm run storybook -- --port 9001
# Clear cache
rm -rf node_modules/.cache/storybook
# Webpack issues
npm run storybook -- --no-manager-cache
# TypeScript issues
npm run storybook -- --no-dll
# Memory issues
NODE_OPTIONS="--max-old-space-size=4096" npm run storybook
Debug Mode¶
# Enable debug logging
DEBUG=storybook:* npm run storybook
# Webpack debug
npm run storybook -- --debug-webpack
# Verbose output
npm run storybook -- --verbose
Performance Issues¶
// .storybook/main.js
module.exports = {
// Disable source maps in development
webpackFinal: async (config) => {
if (process.env.NODE_ENV === 'development') {
config.devtool = false;
}
return config;
},
// Reduce bundle size
features: {
storyStoreV7: true,
buildStoriesJson: true,
},
};
Build Issues¶
# Clear all caches
rm -rf node_modules/.cache
rm -rf storybook-static
npm run build-storybook
# Check for conflicting dependencies
npm ls
# Update Storybook
npx storybook@latest upgrade
Best Practices¶
Story Organization¶
- Hierarchical Structure: Use consistent naming patterns
- Logical Grouping: Group related components together
- Clear Naming: Use descriptive story names
- Documentation: Include comprehensive documentation
Component Development¶
- Isolation: Develop components in isolation
- Props Interface: Design clear, consistent prop interfaces
- Default Values: Provide sensible defaults
- Error Handling: Handle edge cases gracefully
Testing Strategy¶
- Visual Testing: Use visual regression testing
- Interaction Testing: Test user interactions
- Accessibility Testing: Ensure components are accessible
- Unit Testing: Test component logic
Performance Optimization¶
- Lazy Loading: Use story store v7 for lazy loading
- Bundle Splitting: Optimize webpack configuration
- Memory Management: Clean up resources properly
- Build Optimization: Minimize build times
Team Collaboration¶
- Design System: Maintain a consistent design system
- Documentation: Keep documentation up to date
- Code Reviews: Review stories and components
- Automation: Automate testing and deployment
Summary¶
Storybook is an essential tool for modern frontend development that enables:
- Component Isolation: Develop and test UI components independently
- Visual Documentation: Create living documentation for design systems
- Interactive Development: Build components with real-time feedback
- Testing Integration: Visual, interaction, and accessibility testing
- Team Collaboration: Share components across teams and stakeholders
- Design System Management: Maintain consistent UI patterns
Key benefits include faster development cycles, better component quality, improved team collaboration, and comprehensive documentation. Storybook supports all major frontend frameworks and provides extensive customization options through addons and configuration.