Getting started
Installation
# Initialize (auto-detects framework)
npm create storybook@latest
# Dev server
npm run storybook
# Build static
npm run build-storybook
Quick Example
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./Button";
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
primary: true,
label: "Button",
},
};
Configuration Files
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
framework: "@storybook/react-vite",
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-docs", "@storybook/addon-essentials"],
};
export default config;
// .storybook/preview.ts
import type { Preview } from "@storybook/react-vite";
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
/* global decorators */
],
};
export default preview;
Writing Stories (CSF 3)
Basic Story
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./Button";
const meta = {
component: Button,
title: "Components/Button", // Optional
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
primary: true,
label: "Button",
},
};
Story Naming
export const Secondary: Story = {
name: "Secondary Button", // Custom name
args: {
primary: false,
label: "Button",
},
};
Reusing Args
export const Large: Story = {
args: {
...Primary.args,
size: "large",
},
};
export const LargeSecondary: Story = {
args: {
...Secondary.args,
size: "large",
},
};
Default Args
const meta = {
component: Button,
args: {
primary: false, // Default for all stories
disabled: false,
},
} satisfies Meta<typeof Button>;
// Stories inherit defaults
export const Simple: Story = {
args: {
label: "Click me", // Only override what changes
},
};
Args & Controls
ArgTypes
const meta = {
component: Button,
argTypes: {
backgroundColor: {
control: "color",
},
size: {
control: { type: "select" },
options: ["small", "medium", "large"],
},
onClick: {
action: "clicked",
},
},
} satisfies Meta<typeof Button>;
Control Types
| Type | Use Case |
|---|---|
text |
String input |
boolean |
Checkbox |
number |
Number input |
range |
Slider |
color |
Color picker |
date |
Date picker |
object |
JSON editor |
select |
Dropdown |
radio |
Radio buttons |
check |
Checkboxes |
multi-select |
Multiple selection |
Control Options
argTypes: {
variant: {
control: { type: 'radio' },
options: ['primary', 'secondary', 'tertiary'],
},
features: {
control: { type: 'check' },
options: ['icon', 'badge', 'tooltip'],
},
padding: {
control: { type: 'range', min: 0, max: 100, step: 5 },
},
}
Disabling Controls
argTypes: {
onClick: {
control: false, // Hide control
},
internal: {
table: { disable: true }, // Hide from docs
},
}
Custom Rendering
Custom Render Function
export const InContainer: Story = {
args: { label: 'Button' },
render: (args) => (
<div style={{ padding: '3em', backgroundColor: '#f0f0f0' }}>
<Button {...args} />
</div>
),
};
Multiple Components
export const ButtonGroup: Story = {
args: {
label: 'Button',
},
render: (args) => (
<>
<Button {...args} primary />
<Button {...args} />
<Button {...args} disabled />
</>
),
};
With Context
export const Themed: Story = {
args: { label: 'Themed Button' },
render: (args) => (
<ThemeProvider theme="dark">
<Button {...args} />
</ThemeProvider>
),
};
Using Loaded Data
export const WithData: Story = {
render: (args, { loaded: { user } }) => (
<UserProfile {...args} user={user} />
),
};
Decorators
Story-Level Decorator
export const Primary: Story = {
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
],
};
Component-Level Decorator
const meta = {
component: Button,
decorators: [
(Story) => (
<ThemeProvider theme="dark">
<Story />
</ThemeProvider>
),
],
} satisfies Meta<typeof Button>;
Multiple Decorators
const meta = {
component: Button,
decorators: [
(Story) => (
<div className="wrapper"><Story /></div>
),
(Story) => (
<ThemeProvider><Story /></ThemeProvider>
),
],
} satisfies Meta<typeof Button>;
Execution order: Global → Component → Story (outermost → innermost)
Global Decorators
// .storybook/preview.ts
const preview: Preview = {
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
],
};
Parameters
Layout Parameters
export const Primary: Story = {
parameters: {
layout: "centered", // 'centered' | 'fullscreen' | 'padded'
},
};
Background Parameters
export const OnDark: Story = {
parameters: {
backgrounds: {
default: "dark",
values: [
{ name: "dark", value: "#333" },
{ name: "light", value: "#fff" },
],
},
},
};
Docs Parameters
export const Primary: Story = {
parameters: {
docs: {
description: {
story: "Primary button variant",
},
},
},
};
Global Parameters
// .storybook/preview.ts
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
TypeScript
Type Safety with satisfies
import type { Meta, StoryObj } from "@storybook/react-vite";
// ✅ Use satisfies for type safety
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary = {
args: { invalidProp: true }, // ❌ TypeScript error!
} satisfies Story;
Custom Args Types
type PagePropsAndCustomArgs = React.ComponentProps<typeof Page> & {
footer?: string;
};
const meta = {
component: Page,
render: ({ footer, ...args }) => (
<Page {...args}>
<footer>{footer}</footer>
</Page>
),
} satisfies Meta<PagePropsAndCustomArgs>;
Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
const meta = {
component: List<string>,
args: {
items: ['one', 'two', 'three'],
renderItem: (item) => <li key={item}>{item}</li>,
},
} satisfies Meta<typeof List<string>>;
Avoid Type Assertion
// ❌ Bad: 'as' loses type checking
export const Bad = {
args: { invalidProp: true }, // No error!
} as Story;
// ✅ Good: 'satisfies' catches errors
export const Good = {
args: { invalidProp: true }, // TypeScript error!
} satisfies Story;
Interaction Testing
Play Functions
import { expect, userEvent, within } from "@storybook/test";
export const FilledForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emailInput = canvas.getByLabelText("email", {
selector: "input",
});
await userEvent.type(emailInput, "email@example.com", {
delay: 100,
});
const submitButton = canvas.getByRole("button");
await userEvent.click(submitButton);
await expect(canvas.getByText("Success!")).toBeInTheDocument();
},
};
Composing Play Functions
export const Combined: Story = {
play: async (context) => {
await FirstStory.play!(context);
await SecondStory.play!(context);
// Additional interactions
const canvas = within(context.canvasElement);
await userEvent.click(canvas.getByText("Next"));
},
};
Testing Interactions
export const LoginFlow: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Fill form
await userEvent.type(canvas.getByLabelText("Username"), "user");
await userEvent.type(canvas.getByLabelText("Password"), "pass");
// Submit
await userEvent.click(canvas.getByRole("button", { name: /log in/i }));
// Assert result
await expect(canvas.getByText("Welcome!")).toBeInTheDocument();
},
};
Queries
// By role (preferred)
canvas.getByRole("button", { name: /submit/i });
// By label
canvas.getByLabelText("Email");
// By text
canvas.getByText("Hello World");
// By test ID
canvas.getByTestId("custom-element");
Loaders
Async Data Loading
export const WithData: Story = {
loaders: [
async () => ({
user: await fetchUser(),
}),
],
render: (args, { loaded: { user } }) => (
<UserProfile {...args} user={user} />
),
};
Multiple Loaders
export const Dashboard: Story = {
loaders: [
async () => ({
user: await fetchUser(),
}),
async () => ({
posts: await fetchPosts(),
}),
],
render: (args, { loaded: { user, posts } }) => (
<Dashboard {...args} user={user} posts={posts} />
),
};
Global Loaders
// .storybook/preview.ts
const preview: Preview = {
loaders: [
async () => ({
currentUser: await getCurrentUser(),
}),
],
};
MDX Documentation
Basic MDX Story
import { Meta, Canvas, Controls } from "@storybook/blocks";
import * as ButtonStories from "./Button.stories";
<Meta of={ButtonStories} />
# Button
Button component for user actions.
<Canvas of={ButtonStories.Primary} />
<Controls of={ButtonStories.Primary} />
Custom MDX Layout
import { Meta, Canvas, Story } from "@storybook/blocks";
import * as Stories from "./Button.stories";
<Meta of={Stories} />
# Button Component
## Variants
<Canvas>
<Story of={Stories.Primary} />
<Story of={Stories.Secondary} />
</Canvas>
## Usage
```typescript
import { Button } from './Button';
<Button primary label="Click me" />
```
### Documentation Blocks
```mdx
import {
Meta,
Canvas,
Controls,
Description,
Stories,
ArgTypes,
} from '@storybook/blocks';
<Meta of={ButtonStories} />
<Description of={ButtonStories} />
<Canvas of={ButtonStories.Primary} />
<ArgTypes of={ButtonStories} />
<Stories />
```
## Advanced Patterns
<!-- h2-classes: -two-column -->
### Story Templates
```typescript
const Template: Story = {
render: (args) => <Button {...args} />,
};
export const Small: Story = {
...Template,
args: { size: 'small', label: 'Small' },
};
export const Medium: Story = {
...Template,
args: { size: 'medium', label: 'Medium' },
};
```
### Component Variants
```typescript
export const AllVariants: Story = {
render: () => (
<>
<Button variant="primary" label="Primary" />
<Button variant="secondary" label="Secondary" />
<Button variant="tertiary" label="Tertiary" />
</>
),
};
```
### Responsive Stories
```typescript
export const Mobile: Story = {
parameters: {
viewport: {
defaultViewport: 'mobile1',
},
},
};
export const Desktop: Story = {
parameters: {
viewport: {
defaultViewport: 'desktop',
},
},
};
```
### Dark Mode
```typescript
export const DarkMode: Story = {
decorators: [
(Story) => (
<div data-theme="dark">
<Story />
</div>
),
],
parameters: {
backgrounds: { default: 'dark' },
},
};
```
## Gotchas
<!-- h2-classes: -two-column -->
### Always Spread Args
```typescript
// ❌ Bad: Fixed label breaks Controls
export const Bad: Story = {
render: (args) => <Button label="Fixed" />,
};
// ✅ Good: Spread args
export const Good: Story = {
render: (args) => <Button {...args} />,
};
```
### Use satisfies Not as
```typescript
// ❌ Bad: Type assertion silences errors
export const Bad = {
args: { invalidProp: true }, // No error!
} as Story;
// ✅ Good: satisfies catches errors
export const Good = {
args: { invalidProp: true }, // TypeScript error!
} satisfies Story;
```
### Decorator Order Matters
```typescript
// Execution: Global → Component → Story
// Rendering: Story (innermost) → Component → Global (outermost)
const meta = {
decorators: [
(Story) => <Outer><Story /></Outer>, // Renders second
],
};
export const MyStory: Story = {
decorators: [
(Story) => <Inner><Story /></Inner>, // Renders first
],
};
```
### MDX Meta Expects Stories
```typescript
// ❌ Bad: Meta expects story file
import { Button } from './Button';
<Meta of={Button} />
// ✅ Good: Import story file
import * as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
```
### ArgTypes for JSX Values
```typescript
// JSX values need custom argTypes
const meta = {
component: Card,
argTypes: {
icon: {
control: false, // Can't serialize JSX
description: 'Icon component',
},
},
};
```
### CSF 2 Deprecated
```typescript
// ❌ Deprecated: CSF 2 Template.bind({})
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = { label: 'Button' };
// ✅ Modern: CSF 3 args pattern
export const Primary: Story = {
args: { label: 'Button' },
};
```
## Also see
<!-- h2-classes: -one-column -->
- [Storybook Documentation](https://storybook.js.org/docs) _(storybook.js.org)_
- [Writing Stories](https://storybook.js.org/docs/writing-stories) _(storybook.js.org)_
- [Args API](https://storybook.js.org/docs/writing-stories/args) _(storybook.js.org)_
- [Play Functions](https://storybook.js.org/docs/writing-stories/play-function) _(storybook.js.org)_
- [TypeScript Guide](https://storybook.js.org/docs/writing-stories/typescript) _(storybook.js.org)_
- [CSF API](https://storybook.js.org/docs/api/csf) _(storybook.js.org)_
- [ArgTypes API](https://storybook.js.org/docs/api/arg-types) _(storybook.js.org)_