| name | storybook-component |
| description | Component-driven development with Storybook 10. Use when creating or documenting React components, writing stories or interaction tests, designing UI variations, or syncing stories after component changes.
|
Storybook 10 Component Development
Philosophy
Components are designed in Storybook first, then integrated into the app.
Every component gets a .stories.tsx file co-located next to it.
components/
ChatMessage.tsx
ChatMessage.stories.tsx
ChatInput.tsx
ChatInput.stories.tsx
Story File Template (CSF3)
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-bg text-text p-4">
<Story />
</div>
),
],
args: {
onClick: fn(),
},
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
Writing Stories — One Story Per State
Each named export = one visual state. Cover: default, edge cases, loading, error, disabled.
export const Default: Story = {
args: {
label: 'Click me',
variant: 'primary',
},
};
export const Disabled: Story = {
args: {
...Default.args,
disabled: true,
},
};
export const LongContent: Story = {
args: {
label: 'A very long label that might overflow the button container',
},
};
export const Empty: Story = {
args: {
label: '',
},
};
Interaction Testing with Play Functions
Use play functions to verify interactive behavior directly in Storybook.
This replaces basic render tests — no need to duplicate in vitest.
import { expect, fn, within, userEvent } from 'storybook/test';
export const SubmitForm: Story = {
args: {
onSubmit: fn(),
},
play: async ({ args, canvas, userEvent, step }) => {
await step('Fill in form', async () => {
await userEvent.type(canvas.getByLabelText('Email'), 'test@example.com');
await userEvent.type(canvas.getByLabelText('Password'), 'secret123');
});
await step('Submit', async () => {
await userEvent.click(canvas.getByRole('button', { name: 'Submit' }));
});
await expect(args.onSubmit).toHaveBeenCalledOnce();
},
};
Key APIs
| API | Usage |
|---|
canvas.getByRole() | Preferred — query by accessibility role |
canvas.getByLabelText() | Form fields with labels |
canvas.getByText() | Visible text content |
canvas.findByRole() | Async — waits for element to appear |
canvas.queryByRole() | Check element absence (returns null) |
userEvent.click() | Click interaction |
userEvent.type() | Type into input |
userEvent.hover() | Hover over element |
expect() | Jest-compatible assertions |
fn() | Create spy for callback args |
step() | Group interactions with labels |
Custom Render
When the component needs siblings or wrapper context:
export const WithAlert: Story = {
args: { message: 'Hello' },
render: (args) => (
<div className="chat-container">
<MyComponent {...args} />
<footer>Status: connected</footer>
</div>
),
};
Decorators for Providers
Wrap stories with context providers at the meta level:
const meta = {
component: ChatPanel,
decorators: [
(Story) => (
<SocketProvider value={mockSocket}>
<Story />
</SocketProvider>
),
],
} satisfies Meta<typeof ChatPanel>;
Parameters for Addon Config
export const DarkMode: Story = {
parameters: {
backgrounds: { default: 'dark' },
layout: 'fullscreen',
},
};
Testing Strategy Split
| What to test | Where |
|---|
| Visual states & variations | Storybook stories (args) |
| User interactions & UI behavior | Storybook play functions |
| Business logic, hooks, stores | Vitest unit tests |
| Complex async flows | Vitest with mocks |
Rule: If it's about how the component looks or responds to clicks, put it in Storybook.
If it's about logic, put it in vitest.
Storybook Config (React + Vite)
Storybook 10 has essentials and interactions built-in. Only add extra addons as needed.
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
framework: '@storybook/react-vite',
addons: ['@storybook/addon-a11y'],
};
export default config;
import type { Preview } from '@storybook/react-vite';
const preview: Preview = {
parameters: {
layout: 'centered',
},
};
export default preview;
Naming Conventions
- File:
ComponentName.stories.tsx (co-located with component)
- Meta title: auto-derived from file path, or explicit
title: 'Components/ChatMessage'
- Story names: PascalCase describing the state:
Default, Loading, WithError, LongContent
Checklist for New Components
- Create component file with props interface
- Create
.stories.tsx with meta + Default story
- Add story variants for each meaningful state
- Add play functions for interactive behavior
- Verify in Storybook UI (
pnpm storybook)
- Add vitest tests only for logic not covered by stories