Storybook
Storybook is a workshop for building UI components in isolation, outside the noise of your full application. Instead of clicking through five screens to reach the one button state you want to tweak, you render that component directly with the exact props it needs. The result is faster iteration, living documentation, and a stable surface for visual and interaction testing. For any non-trivial React design system, Storybook becomes the canonical place where components are developed, reviewed, and verified.
Why isolation matters
When a component lives only inside a running app, every variation is hard to reach. Loading states, error states, empty states, and rare prop combinations all hide behind routing, data fetching, and authentication. Storybook flips this around: each meaningful state of a component is captured as a story — a small, declarative description of how to render it. You browse those states in a sidebar, edit them live, and share a deployed Storybook URL with designers and QA without anyone running the backend.
Getting started
Storybook detects your framework and bundler automatically. In an existing Vite + React project, run the installer:
npx storybook@latest init
This adds the dependencies, creates a .storybook/ config folder, and writes example stories. Start the dev server with:
npm run storybook
Output:
Storybook 8.6.0 started
Local: http://localhost:6006/
On your network: http://192.168.1.10:6006/
Writing your first story
A story is a named export describing one rendered state of a component. The modern format is CSF 3 (Component Story Format), which uses a default export for metadata and named exports for each variation.
Given a simple component:
// Button.jsx
export function Button({ variant = "primary", disabled = false, children, onClick }) {
return (
<button
className={`btn btn--${variant}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
The story file lives next to it:
// Button.stories.jsx
import { fn } from "@storybook/test";
import { Button } from "./Button";
export default {
title: "Components/Button",
component: Button,
args: { onClick: fn(), children: "Click me" },
argTypes: {
variant: { control: "select", options: ["primary", "secondary", "danger"] },
disabled: { control: "boolean" },
},
};
export const Primary = {};
export const Danger = {
args: { variant: "danger", children: "Delete" },
};
export const Disabled = {
args: { disabled: true },
};
Each named export inherits the args from the default export and overrides only what changes. The fn() helper creates a spy so clicks are logged in the Actions panel and assertable in tests.
Controls and args
Args are the inputs to a story, and argTypes describe how Storybook renders editing controls for them. The Controls addon turns these into a live panel where anyone can adjust props without touching code — the canvas re-renders instantly.
| Control type | Use for | Example |
|---|---|---|
boolean | Toggles | disabled, loading |
select / radio | Fixed option sets | variant, size |
text | Free-form strings | labels, placeholders |
number / range | Numeric values | count, width |
color | Color pickers | theme tokens |
object | Structured data | a user prop |
Tip: Set sensible default
argsat the meta level so every story starts in a realistic state. Stories that override nothing (export const Primary = {};) then document the component’s baseline behavior for free.
Documentation pages
Storybook auto-generates a Docs tab for each component from its props, JSDoc comments, and stories. For richer narrative pages, add an MDX file that mixes prose with live story embeds:
import { Meta, Story, Controls } from "@storybook/blocks";
import * as ButtonStories from "./Button.stories";
<Meta of={ButtonStories} />
# Button
Use buttons to trigger actions. Prefer `primary` for the main action on a view.
<Story of={ButtonStories.Primary} />
<Controls />
Because the docs render the same stories you develop against, they never drift out of sync with the real component.
Interaction and visual testing
Stories double as test fixtures. The interaction testing addon lets you script user behavior in a play function that runs in the browser after the story mounts:
import { expect, userEvent, within, fn } from "@storybook/test";
import { Button } from "./Button";
export default { component: Button, args: { onClick: fn() } };
export const ClickFires = {
args: { children: "Submit" },
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: "Submit" });
await userEvent.click(button);
await expect(args.onClick).toHaveBeenCalledTimes(1);
},
};
These play functions show step-by-step in the Interactions panel and can run headlessly in CI via the test runner. Pair them with a visual testing tool (such as Chromatic) to catch unintended pixel changes — every story becomes a snapshot baseline, so a stray CSS edit surfaces as a diff in your pull request.
Best practices
- Co-locate
*.stories.jsxfiles next to their components so stories move and refactor with the code. - Write one story per meaningful state — default, loading, error, empty, disabled — rather than cramming variations into a single story.
- Define shared inputs as meta-level
argsand override only what differs in each story to keep them DRY. - Use the
playfunction to encode the behavior you’d otherwise test manually, then run them in CI with the test runner. - Mock data and context with decorators instead of reaching into real APIs, keeping every story deterministic.
- Deploy your Storybook on each PR so designers and reviewers see component changes without checking out the branch.