| name | storybook-patterns |
| description | Storybook patterns: CSF3 (meta satisfies Meta, play functions, @storybook/test), addon ecosystem (a11y, interactions, docs), MSW integration for API mocking, Chromatic CI, storybook-test-runner for Jest/Playwright execution, and Storybook as living documentation. |
Storybook Patterns
Component development environment and living documentation system.
When to Activate
- Writing stories for a new component (CSF3 format)
- Setting up interaction tests with
play functions
- Configuring MSW for API mocking in stories
- Setting up Chromatic for visual regression
- Auditing existing Storybook setup
- Building automated accessibility checks into stories
- Migrating legacy CSF2 stories to CSF3 with
satisfies Meta for full type safety
- Running stories as automated tests in CI using
@storybook/test-runner and axe-playwright
Component Story Format 3 (CSF3)
The current standard — no default exports, satisfies for type safety.
Basic Story
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
description: 'Visual variant of the button',
},
onClick: { action: 'clicked' },
},
tags: ['autodocs'],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me',
},
};
export const Disabled: Story = {
args: {
variant: 'primary',
children: 'Disabled',
disabled: true,
},
};
export const Loading: Story = {
args: {
children: 'Loading...',
loading: true,
},
};
Play Functions (Interaction Tests)
import { within, userEvent, expect } from '@storybook/test';
import type { Meta, StoryObj } from '@storybook/react';
import { LoginForm } from './LoginForm';
const meta = {
component: LoginForm,
tags: ['autodocs'],
} satisfies Meta<typeof LoginForm>;
export default meta;
type Story = StoryObj<typeof meta>;
export const SuccessfulLogin: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Fill in credentials', async () => {
await userEvent.type(
canvas.getByLabelText('Email'),
'user@example.com',
{ delay: 50 }
);
await userEvent.type(
canvas.getByLabelText(),
,
{ : }
);
});
(, () => {
userEvent.(canvas.(, { : }));
});
(, () => {
(
canvas.()
).();
});
},
};
: = {
: ({ canvasElement }) => {
canvas = (canvasElement);
userEvent.(canvas.(, { : }));
(canvas.()).();
(canvas.()).();
},
};
Addons
@storybook/addon-a11y — Accessibility
npm install --save-dev @storybook/addon-a11y
const config = {
addons: [
'@storybook/addon-a11y',
],
};
export const DecorativeIcon: Story = {
parameters: {
a11y: {
disable: true,
},
a11y: {
config: {
rules: [{ id: 'color-contrast', enabled: false }],
},
},
},
};
@storybook/addon-interactions — Visual Interaction Tests
npm install --save-dev @storybook/addon-interactions @storybook/test
Interaction tests run in the Storybook UI with step-by-step playback:
- "Step 1: Fill email" → show state
- "Step 2: Submit" → show state
- "Step 3: Verify success" → pass/fail
@storybook/test-runner — Run Stories as Tests
npm install --save-dev @storybook/test-runner
{
"scripts": {
"test-storybook": "test-storybook"
}
}
import type { TestRunnerConfig } from '@storybook/test-runner';
import { checkA11y, injectAxe } from 'axe-playwright';
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await checkA11y(page, '#storybook-root', {
detailedReport: true,
detailedReportOptions: { html: true },
});
},
};
export default config;
npm run build-storybook -- --quiet
npx http-server storybook-static --port 6006 &
npm run test-storybook
MSW Integration (Mock Service Worker)
Mock API calls in stories without changing implementation code.
npm install msw msw-storybook-addon --save-dev
npx msw init public/ --save
import { initialize, mswLoader } from 'msw-storybook-addon';
initialize();
export default {
loaders: [mswLoader],
};
import { http, HttpResponse } from 'msw';
export const WithData: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/products', () => {
return HttpResponse.json([
{ id: 1, name: 'Widget Pro', price: 49.99 },
{ id: 2, name: 'Widget Lite', price: 9.99 },
]);
}),
],
},
},
};
export const Empty: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/products', () => HttpResponse.json([])),
],
},
},
};
export const Error: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/products',
.({ : }, { : })
),
],
},
},
};
Chromatic CI Integration
name: Chromatic
on:
push:
branches: [main]
pull_request:
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: true
exitZeroOnChanges: false
autoAcceptChanges: main
npx chromatic --project-token=<token>
npx chromatic --project-token=<token> --only-changed
npx chromatic --project-token=<token> --force-rebuild
Storybook as Living Documentation
autodocs
export const parameters = {
docs: {
autodocs: 'tag',
},
};
const meta = {
tags: ['autodocs'],
} satisfies Meta<typeof Component>;
ArgTypes Documentation
const meta = {
component: DatePicker,
argTypes: {
value: {
description: 'Currently selected date',
control: 'date',
table: {
type: { summary: 'Date | null' },
defaultValue: { summary: 'null' },
},
},
onChange: {
description: 'Called when user selects a date',
action: 'date-changed',
table: { type: { summary: '(date: Date) => void' } },
},
locale: {
description: 'BCP 47 language tag for date formatting',
control: 'text',
table: {
type: { summary: 'string' },
defaultValue: { summary: '"en-US"' },
},
},
},
} satisfies Meta<typeof DatePicker>;
Story Descriptions
export const WithCustomLocale: Story = {
name: 'Localized (German)',
parameters: {
docs: {
description: {
story: 'Date picker configured for German locale — uses DD.MM.YYYY format and German month names.',
},
},
},
args: {
locale: 'de-DE',
},
};
.storybook/main.ts Configuration
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-interactions',
'msw-storybook-addon',
'@chromatic-com/storybook',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
typescript: {
check: true,
},
};
export default config;
CSF2 → CSF3 Migration
export default {
title: 'Components/Button',
component: Button,
};
export const Primary = (args) => <Button {...args} />;
Primary.args = { variant: 'primary', children: 'Click me' };
import type { Meta, StoryObj } from '@storybook/react';
const meta = { component: Button } satisfies Meta<typeof Button>;
export default meta;
export const Primary: StoryObj<typeof meta> = {
args: { variant: 'primary', children: 'Click me' },
};
Reference
visual-testing — Chromatic setup, Playwright screenshots, baseline management
e2e-testing — Playwright functional tests (not visual)
accessibility — WCAG guidelines the a11y addon checks against