Storybook Cheat Sheet
Component story format (CSF3), args, decorators, addons, and interaction testing syntax for building an isolated UI component workshop.
CSF3 Story File
Modern Component Story Format: default export is meta, named exports are stories.
// Button.stories.tsximport type { Meta, StoryObj } from '@storybook/react'import { Button } from './Button'const meta: Meta<typeof Button> = { title: 'Components/Button', component: Button, args: { children: 'Click me' }, argTypes: { variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] }, },}export default metatype Story = StoryObj<typeof Button>export const Primary: Story = { args: { variant: 'primary' } }export const Disabled: Story = { args: { disabled: true } }
Decorators & Providers
Wrap every story in a component's file (or globally) with required context/providers.
const meta: Meta<typeof Cart> = { component: Cart, decorators: [ (Story) => ( <ThemeProvider theme="light"> <Story /> </ThemeProvider> ), ],}// .storybook/preview.tsx — applies globallyexport const decorators = [ (Story) => <QueryClientProvider client={queryClient}><Story /></QueryClientProvider>,]
Play Function (Interaction Testing)
Simulate user interactions and assert on results directly inside the story.
import { within, userEvent, expect } from '@storybook/test'export const SubmitsForm: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement) await userEvent.type(canvas.getByLabelText('Email'), '[email protected]') await userEvent.click(canvas.getByRole('button', { name: /submit/i })) await expect(canvas.getByText('Success')).toBeInTheDocument() },}
Mocking API Calls with msw
Use Mock Service Worker addon to intercept requests inside a story.
import { http, HttpResponse } from 'msw'export const LoadedState: Story = { parameters: { msw: { handlers: [ http.get('/api/user', () => HttpResponse.json({ name: 'Ana' })), ], }, },}
CLI & Config Essentials
Commands and files you touch on every project.
- npx storybook@latest init- bootstrap Storybook into an existing project, auto-detects framework
- npm run storybook- runs the dev server, default port 6006
- npm run build-storybook- builds a static Storybook site for deployment
- .storybook/main.ts- addons list, stories glob pattern, framework config
- .storybook/preview.ts- global parameters, decorators, and argTypes applied to all stories
- chromatic (addon)- visual regression testing/hosting service built for Storybook
Portable Stories in Vitest/Jest
Reuse a story's args, decorators, and play function directly inside a unit test without spinning up Storybook.
// Button.test.tsximport { composeStories } from '@storybook/react'import { render, screen } from '@testing-library/react'import * as stories from './Button.stories'const { Primary, Disabled } = composeStories(stories)test('Primary renders and runs its play function', async () => { await Primary.run() expect(screen.getByRole('button')).toBeEnabled()})test('Disabled button cannot be clicked', () => { render(<Disabled />) expect(screen.getByRole('button')).toBeDisabled()})
globalTypes + Toolbar (Theme/Locale Switcher)
Add a toolbar control that every story can react to via a decorator, without touching individual stories.
// .storybook/preview.tsxexport const globalTypes = { theme: { description: 'Global theme for components', toolbar: { icon: 'paintbrush', items: [ { value: 'light', title: 'Light' }, { value: 'dark', title: 'Dark' }, ], dynamicTitle: true, }, },}export const decorators = [ (Story, context) => ( <div data-theme={context.globals.theme}> <Story /> </div> ),]
Storybook Test Runner in CI
Execute every story's play function headlessly and assert accessibility, wired into a CI pipeline.
# .github/workflows/storybook-tests.ymljobs: test-storybook: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx playwright install --with-deps - run: npm run build-storybook --quiet - run: npx concurrently -k -s first \ "npx http-server storybook-static --port 6006 --silent" \ "npx wait-on tcp:6006 && npx test-storybook --url http://localhost:6006"
Registering a Minimal Custom Addon
Add a panel to the Storybook UI using the addons API, e.g. to surface component metadata.
// my-addon/register.tsximport { addons, types } from '@storybook/manager-api'import { AddonPanel } from '@storybook/components'import React from 'react'addons.register('my/design-tokens', () => { addons.add('my/design-tokens/panel', { type: types.PANEL, title: 'Design Tokens', render: ({ active }) => ( <AddonPanel active={!!active}> <pre>{JSON.stringify({ spacing: '4px base' }, null, 2)}</pre> </AddonPanel> ), })})
Advanced main.ts Config Keys
Options that matter once a Storybook instance grows past a starter template.
- typescript.reactDocgen- 'react-docgen-typescript' extracts prop tables/JSDoc into the Controls addon automatically
- staticDirs- array of folders (fonts, mock images) served as-is alongside the built Storybook
- core.disableTelemetry- turn off Storybook's anonymous usage analytics, common in regulated environments
- framework.options.builder- swap Webpack5 for Vite for dramatically faster dev-server startup on large story counts
- docs.autodocs- 'tag' generates an autodocs page only for stories tagged 'autodocs' rather than every component
- features.buildStoriesJson- emits stories.json used by external tools (e.g. cross-repo Storybook composition) to index stories
Write stories for edge/error states (empty list, loading, error, very long text) as first-class exports, not just the happy path — those are exactly the states that are hardest to reproduce manually and where Storybook's isolation pays off most.