| name | storybook |
| description | Use this skill when configuring or writing stories in Storybook 8+. Covers setup for Vite and Next.js, CSF3, controls, play functions, accessibility addon, provider decorators, design token integration, dark mode, and visual testing with test-storybook in CI.
|
Storybook — Configuration and Stories
Agent workflow
- Install Storybook and addons based on project type (section Setup).
- Configure
preview.ts with global decorators, global CSS, and tokens (section Configuration).
- Write stories in CSF3 with typed
args and tags: ['autodocs'] (section CSF3 Structure).
- Add
play functions to every interactive component (section Play Functions).
- Verify that
@storybook/addon-a11y reports no AA violations.
- Satisfy all 10 mandatory rules before marking the task complete.
Mandatory Cross-references
Setup
Vite + React
pnpm dlx storybook@latest init --type react --builder vite
pnpm add -D @storybook/addon-a11y @storybook/test @storybook/addon-interactions \
@storybook/addon-themes @storybook/test-runner
Next.js
pnpm dlx storybook@latest init --type nextjs
pnpm add -D @storybook/addon-a11y @storybook/test @storybook/addon-interactions \
@storybook/addon-themes @storybook/test-runner
.storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.{ts,tsx}'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-interactions',
'@storybook/addon-themes',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;
Global Configuration (preview.ts)
import type { Preview } from '@storybook/react';
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import '../src/index.css';
const preview: Preview = {
parameters: {
a11y: {
config: { rules: [{ id: 'color-contrast', enabled: true }] },
},
controls: { matchers: { color: /(background|color)$/i, date: /Date$/ } },
layout: 'centered',
},
decorators: [
withThemeByDataAttribute({
themes: { light: '', dark: 'dark' },
defaultTheme: 'light',
attributeName: 'data-theme',
}),
],
};
export default preview;
CSF3 Structure
Standard format for every story in the project:
import type { Meta, StoryObj } from '@storybook/react';
import { ComponentName } from './ComponentName';
const meta = {
title: 'Atoms/ComponentName',
component: ComponentName,
tags: ['autodocs'],
args: {
children: 'Example text',
},
} satisfies Meta<typeof ComponentName>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Disabled: Story = { args: { disabled: true } };
export const Loading: Story = { args: { isLoading: true } };
export const Error: Story = { args: { state: 'error' } };
title Naming Convention
Atoms/Button → individual atom
Molecules/FormField → molecule
Organisms/DataTable → organism
Patterns/AuthModal → reusable business pattern
ArgTypes — Interactive Controls
Storybook infers controls from TypeScript. Override only when inference is unclear:
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'destructive'],
description: 'Visual variant of the button',
table: { defaultValue: { summary: 'primary' } },
},
size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] },
isLoading: { control: 'boolean' },
onClick: { action: 'clicked' },
},
Play Functions — Interaction Testing
play functions are tests that run inside Storybook and in CI with test-storybook. Use the same patterns as React Testing Library:
import { expect, userEvent, within } from '@storybook/test';
export const OpensDropdown: Story = {
args: { label: 'Options' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const trigger = canvas.getByRole('button', { name: /options/i });
await userEvent.click(trigger);
const menu = canvas.getByRole('menu');
await expect(menu).toBeVisible();
await userEvent.keyboard('{ArrowDown}');
await expect(canvas.getAllByRole('menuitem')[0]).toHaveFocus();
await userEvent.keyboard('{Escape}');
await expect(menu).not.toBeVisible();
await expect(trigger).toHaveFocus();
},
};
export const SubmitsForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText(/email/i), 'user@example.com');
await userEvent.type(canvas.getByLabelText(/password/i), 'password123');
await userEvent.click(canvas.getByRole('button', { name: /sign in/i }));
await expect(canvas.getByText(/welcome/i)).toBeInTheDocument();
},
};
Running in CI
pnpm storybook build
pnpm concurrently -k -s first \
"pnpm http-server storybook-static --port 6006 --silent" \
"pnpm wait-on tcp:6006 && pnpm test-storybook"
A11y Addon
The addon runs axe-core on each story automatically. The "Accessibility" panel shows violations. Never disable color-contrast — if it fails, fix the color token.
export const WithException: Story = {
parameters: {
a11y: {
config: {
rules: [
{ id: 'aria-required-parent', enabled: false },
],
},
},
},
};
Provider Decorators
Global providers go in preview.ts. Story-specific providers go in the story's decorators field.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import i18n from '../src/config/i18n';
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
export const decorators = [
(Story) => (
<MemoryRouter>
<I18nextProvider i18n={i18n}>
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
</I18nextProvider>
</MemoryRouter>
),
];
export const SettingsPage: Story = {
decorators: [
(Story) => (
<MemoryRouter initialEntries={['/dashboard/settings']}>
<Story />
</MemoryRouter>
),
],
};
Design Tokens in Storybook
Importing index.css in preview.ts is enough for CSS custom properties. For a visual tokens story:
import type { Meta, StoryObj } from '@storybook/react';
import { TokensGrid } from './TokensGrid';
export default {
title: 'Design Tokens/Colors',
component: TokensGrid,
tags: ['autodocs'],
} satisfies Meta<typeof TokensGrid>;
To make the Docs theme use the same DS colors:
parameters: {
docs: {
theme: {
colorPrimary: 'var(--color-primary)',
colorSecondary: 'var(--color-muted)',
},
},
},
Mandatory Rules
tags: ['autodocs'] on every meta — generates a Docs tab with props table and live controls.
- One story per semantic state —
Default, Disabled, Loading, Error are separate stories.
play function on every interactive component — dropdowns, modals, forms, tabs, accordions.
addon-a11y with no AA violations — do not merge stories with active violations.
- Never disable
color-contrast — fix the color token, not the rule.
- Providers in decorators, never in
render — keep stories declarative.
- No business logic in stories — stories render props, they do not simulate app flows.
args shared in meta.args — do not repeat the same arg in every story.
title follows DS hierarchy — Atoms/, Molecules/, Organisms/, Patterns/.
- Events as
action — register onClick, onChange, onSubmit in argTypes for the Actions panel.
Gotchas
- Using
@storybook/testing-library (deprecated) — migrate to the unified @storybook/test API in Storybook 8.
document.querySelector in play functions — always use within(canvasElement) to avoid story cross-contamination.
- Global CSS not imported in
preview.ts — tokens and Tailwind reset won't apply; stories look different from the app.
autodocs tag missing — the Docs tab won't appear for that component in the published Storybook.
- Providers hardcoded in
render — makes it impossible to override the provider in interaction tests.
- Test-runner not configured in CI —
play functions only run locally; the pipeline won't catch regressions.