| name | storybook-react-guidelines |
| description | Storybook guidelines for React including story structure, interaction tests with play functions, and Testing Library queries. Auto-loaded when working with story files. |
| category | guideline |
| user-invocable | false |
Storybook Guidelines (React)
Overview
Storybook is used for:
- Component development in isolation
- Visual documentation of component states
- Interaction testing via play functions
- Accessibility auditing
- Visual regression testing
Story File Structure
Meta Configuration
import type { Meta, StoryObj } from '@storybook/react';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
title: 'Category/Subcategory/MyComponent',
component: MyComponent,
parameters: { layout: 'centered' },
tags: ['autodocs'],
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'danger'] },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
Story Title Organization
title: 'Components/Forms/TextInput'
title: 'Views/Dashboard/Overview'
title: 'Primitives/Controls/Button'
Basic Stories
export const Default: Story = {
args: { label: 'Click me', variant: 'primary' },
};
export const Secondary: Story = {
args: { ...Default.args, variant: 'secondary' },
};
export const WithIcon: Story = {
args: { label: 'Save', icon: 'save' },
render: (args) => (
<div style={{ padding: '20px' }}>
<MyComponent {...args} />
</div>
),
};
Interaction Tests with play()
Basic Structure
import { expect, userEvent, within, waitFor } from '@storybook/test';
export const Interactive: Story = {
args: { label: 'Submit' },
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Click the button', async () => {
const button = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(button);
});
await step('Verify state change', async () => {
await waitFor(() => {
expect(canvas.getByText('Submitted')).toBeInTheDocument();
});
});
},
};
Query Strategies
Component-scoped queries:
const canvas = within(canvasElement);
const button = canvas.getByRole('button', { name: 'Submit' });
Global queries (for modals, toasts, dropdowns):
import { screen } from '@storybook/test';
const modal = screen.getByRole('dialog');
const toast = screen.getByRole('status');
Testing Library Query Priority
For Testing Library query priority, see vitest-guidelines.
Async Handling
Always use waitFor for async assertions:
await waitFor(() => {
expect(canvas.getByText('Success')).toBeInTheDocument();
});
Check for element removal:
await waitFor(() => {
expect(canvas.queryByRole('alert')).not.toBeInTheDocument();
});
Best Practices
Story Naming and Organization
export const Default: Story = { ... };
export const Disabled: Story = { ... };
export const WithError: Story = { ... };
export const Loading: Story = { ... };
export const UserFlow: Story = {
play: async ({ canvasElement, step }) => { ... },
};
export const Story1: Story = { ... };
export const Test: Story = { ... };
Test Plan Alignment
Every story with a play() function should have a corresponding test plan (see Meta Configuration example above for format).
Common Pitfalls
- Missing waitFor — Always wrap async assertions in
waitFor() after user interactions to avoid race conditions
- Wrong query scope — Use
within(canvasElement) for component queries; use screen only for teleported elements (modals, toasts)
- Toast vs Alert roles — Success notifications use
role="status", errors use role="alert"
- Global queries in component scope — Modals/dropdowns are teleported outside the component; query them via
screen, not canvas
Running Storybook Tests
npm run storybook
npm run test-storybook
npm run test-storybook -- --grep "ComponentName"
npm run test-storybook -- --coverage
Additional References