storybook
A frontend workshop that runs your UI components in isolation, outside your app. You describe each state of a component as a story (a small exported object), and Storybook serves a browsable catalog of those states with knobs for props, generated docs, and interaction tests that click through the rendered component. It supports React, Vue 3, Angular, Svelte, web components, and more, and doubles as the shared reference for design systems: developers build against it, QA tests against it, designers review in it.
The default component workshop, and clearly worth it for design systems and component-heavy teams. For a small app it is a second build system you have to babysit; Ladle or plain Vitest may be the better trade.
Use it if
- You maintain a design system or shared component library and need a living, clickable catalog of every component state
- You want interaction tests (play functions) and visual regression running against real rendered components, not jsdom approximations
- Designers, PMs, or QA need to review component states without running the whole app locally
- You want prop documentation generated from your components via autodocs instead of hand-written docs that drift
- Your app has a couple dozen components and no shared consumers; writing and maintaining stories will cost more than it pays back
- Storybook runs its own builder with its own config, so aliases, Tailwind, and env handling often need to be wired a second time in .storybook and can break independently of your app
- You mainly need logic and unit coverage; Vitest plus Testing Library covers that without a second dev server and a fleet of @storybook/* packages in your lockfile
- CI time is tight; building a large Storybook is slow, and majors arrive roughly yearly with codemods you have to actually run
Setup reality
npx storybook@latest init detects your framework, scaffolds a .storybook directory and example stories, and usually works on the happy path (Vite plus React or Vue). The honest part: it installs a cluster of @storybook/* packages that must stay on identical versions, so you upgrade with npx storybook@latest upgrade rather than editing package.json by hand. Because Storybook has its own builder pipeline, anything custom in your app build (path aliases, PostCSS, Tailwind, global providers) has to be mirrored in .storybook/main and .storybook/preview. Monorepos and nonstandard setups are where init falls over and you end up reading framework-specific docs.
Patterns
Add Storybook to an existing appinit-project
npx storybook@latest init
npm run storybook # dev server on :6006Run it inside the app so it can detect your framework and builder; on monorepos run it in the package, not the repo root.
Write a CSF3 storybasic-story
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./Button";
const meta = {
title: "UI/Button",
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: { variant: "primary", children: "Save" },
};Since Storybook 9, import types from your framework package (@storybook/react-vite here), not the bare renderer package.
Multiple states of one componentstory-variants
export const Primary: Story = {
args: { variant: "primary", children: "Save" },
};
export const Disabled: Story = {
args: { ...Primary.args, disabled: true },
};Spread another story's args to avoid repeating the base props; each named export becomes a sidebar entry.
Customize the controls panelcontrols-argtypes
const meta = {
component: Button,
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "ghost"],
},
onClick: { action: "clicked" },
},
} satisfies Meta<typeof Button>;Controls are inferred from prop types automatically; argTypes is only needed to override the inferred widget.
Wrap stories in a provider or layoutdecorator-wrapper
const meta = {
component: UserCard,
decorators: [
(Story) => (
<ThemeProvider theme="light">
<div style={{ padding: 24 }}>
<Story />
</div>
</ThemeProvider>
),
],
} satisfies Meta<typeof UserCard>;Decorators on meta apply to every story in the file; put app-wide providers in .storybook/preview instead.
Interaction test with a play functioninteraction-test
import { expect, fn, userEvent, within } from "storybook/test";
export const SubmitsForm: Story = {
args: { onSubmit: fn() },
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText("Email"), "a@b.co");
await userEvent.click(canvas.getByRole("button", { name: "Submit" }));
await expect(args.onSubmit).toHaveBeenCalledOnce();
},
};Since v9 the test utilities live in storybook/test (no @); old imports from @storybook/test or @storybook/jest fail after upgrade.
Spy on a callback propmock-callback-spy
import { fn } from "storybook/test";
export const Clickable: Story = {
args: { onClick: fn() },
};fn() spies show every call in the Actions panel and are assertable inside play functions.
Generate a docs page from storiesautodocs
const meta = {
component: Button,
tags: ["autodocs"],
} satisfies Meta<typeof Button>;Adds a Docs entry with a prop table and all stories; set the tag globally in .storybook/preview to enable it everywhere.
Global parameters and decoratorsglobal-config
// .storybook/preview.ts
import type { Preview } from "@storybook/react-vite";
const preview: Preview = {
parameters: {
backgrounds: { default: "dark" },
layout: "centered",
},
};
export default preview;preview.ts is for what stories render with; .storybook/main.ts is for addons, framework, and builder config. Mixing them up is the classic config error.
Build a deployable static Storybookbuild-static
npx storybook build
# output in storybook-static/, host it anywhere staticThe static build is how you share with designers and run visual regression in CI; it can be slow on big projects, so cache node_modules and the .cache dir.
Upgrade Storybook safelyupgrade-version
npx storybook@latest upgradeAlways use the upgrade command instead of bumping versions by hand; it runs codemods and keeps every @storybook/* package on the same version.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ladle | npm | React-only teams on Vite who want stories with a fraction of the install and startup weight |
| histoire | npm | Vue or Svelte teams on Vite who want a native-feeling story workshop |
| react-cosmos | npm | You want a component playground driven by fixtures rather than the full Storybook docs-and-addons machine |