storybook
Storybook
Overview
Storybook is a frontend workshop for building UI components and pages in isolation. It helps develop and share hard-to-reach states and edge cases without running the whole app. Use it for UI development, testing, and documentation.
Core concepts: Stories (rendered component states), CSF (Component Story Format), args (props/inputs), decorators (wrappers), play functions (interaction tests).
Requirements: Node.js 20+, npm 10+ or pnpm 9+ or Yarn 4+, Vite 5+ or Webpack 5+, TypeScript 4.9+ (optional).
Installation
Run inside the project root:
npm create storybook@latest
The CLI detects the framework (Next.js, Vite, React, Vue, Angular, Svelte, etc.) and configures accordingly. For manual framework selection:
npm create storybook@latest --type nextjs
# or: react, vue3, angular, svelte, sveltekit, etc.
Recommended: Use @storybook/nextjs-vite for Next.js projects (faster than Webpack). Use @storybook/react-vite for Vite + React.
After install, run: npm run storybook (dev server, typically port 6006). Build: npm run build-storybook.
Project structure: system directory
Place components in a system/ directory, with each component in its own folder. Colocate the story file alongside the component — this keeps documentation and tests next to the source and follows Storybook best practices.
system/
├── Button/
│ ├── Button.tsx
│ └── Button.stories.tsx
├── Card/
│ ├── Card.tsx
│ └── Card.stories.tsx
├── Dialog/
│ ├── Dialog.tsx
│ └── Dialog.stories.tsx
Configure .storybook/main.ts to load stories from system/:
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../system/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
};
export default config;
Component Story Format (CSF)
Stories use CSF 3: a default export (meta) and named exports (stories).
Meta (default export)
import type { Meta } from '@storybook/your-framework';
import { Button } from './Button';
const meta = {
component: Button,
title: 'System/Button', // optional; auto-derived from path if omitted
} satisfies Meta<typeof Button>;
export default meta;
Stories (named exports)
import type { StoryObj } from '@storybook/your-framework';
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
label: 'Button',
primary: true,
},
};
export const Secondary: Story = {
args: {
...Primary.args,
primary: false,
label: 'Secondary',
},
};
Args map to component props. They appear in the Controls panel for live editing.
Writing stories
Basic story with args
export const Default: Story = {
args: {
label: 'Click me',
disabled: false,
},
};
Custom render
Use render when the default (component + args) is not enough:
export const InAlert: Story = {
args: { label: 'Confirm' },
render: (args) => (
<Alert>
<Button {...args} />
</Alert>
),
};
Play function (interaction tests)
Simulate user interactions and assert outcomes:
import { expect } from 'storybook/test';
export const OpensDialog: Story = {
play: async ({ canvas, userEvent }) => {
await userEvent.click(canvas.getByRole('button', { name: 'Open' }));
await expect(canvas.getByRole('dialog')).toBeInTheDocument();
},
};
Decorators
Wrap stories (e.g. padding, theme, providers):
const meta = {
component: Button,
decorators: [
(Story) => (
<div style={{ padding: '3em' }}>
<Story />
</div>
),
],
} satisfies Meta<typeof Button>;
Parameters
Static metadata for addons (backgrounds, layout, etc.):
const meta = {
component: Button,
parameters: {
backgrounds: { values: [{ name: 'dark', value: '#333' }] },
},
} satisfies Meta<typeof Button>;
Reusing stories across components
Import story args from child components to compose parent stories:
import * as ButtonStories from '../Button/Button.stories';
export const WithButtons: Story = {
args: {
buttons: [
{ ...ButtonStories.Primary.args },
{ ...ButtonStories.Secondary.args },
],
},
};
Configuration
main.ts
- stories: Glob patterns for story files.
- addons: e.g.
@storybook/addon-essentials,@storybook/addon-a11y. - framework: e.g.
@storybook/nextjs-vite,@storybook/react-vite. - staticDirs: Static assets (e.g.
['../public']).
preview.ts
Global decorators, parameters, and CSS:
import type { Preview } from '@storybook/react';
import '../app/globals.css';
const preview: Preview = {
decorators: [
(Story) => (
<div style={{ fontFamily: 'system-ui' }}>
<Story />
</div>
),
],
parameters: {
layout: 'centered',
},
};
export default preview;
Testing
- Render tests: Every story is a smoke test — it passes if it renders without error.
- Interaction tests: Use
playwithexpectfromstorybook/test. - Vitest addon:
npx storybook add @storybook/addon-vitest— runs stories as Vitest tests. - Accessibility: Enable
@storybook/addon-a11yfor automated a11y checks. - Visual testing: Use Chromatic or snapshot tests for visual regression.
Best practices
- Colocate stories with components in
system/<ComponentName>/. - Use args instead of hardcoding props; enables Controls and reuse.
- One story per meaningful state (default, loading, error, empty, etc.).
- Reuse child story args when composing parent stories.
- Use
playfor interaction tests instead of manual E2E when possible. - Keep stories focused — one component per story file; use
renderfor composition.
Common mistakes
- Stories in separate folder: Prefer colocation (
Component.stories.tsxnext toComponent.tsx). - Missing
componentin meta: Required for autodocs and Controls; usesatisfies Meta<typeof Component>. - Hardcoded props: Use
argsso Controls work and stories are reusable. - Wrong framework import: Use
@storybook/nextjs-vite,@storybook/react-vite, etc., not generic@storybook/react. - Glob not matching system/: Ensure
storiesinmain.tsincludes../system/**/*.stories.*.
Additional resources
- reference.md — Official Storybook docs links, frameworks, addons, testing.
- Official: https://storybook.js.org/docs
- Writing stories: https://storybook.js.org/docs/writing-stories
- Testing: https://storybook.js.org/docs/writing-tests