Storybook 8 Reference: Stories, Controls, Interactions, MSW Mocking & Visual Testing
Storybook 8 is a frontend workshop for developing, documenting, and testing UI components in isolation. Each story is a named render of a component with specific props — you can develop components without spinning up the whole app, test every visual state, and auto-generate documentation. Storybook 8 added first-class Vitest integration, so stories double as component tests. The key concept: stories are the source of truth for component states; everything else (docs, visual tests, interaction tests) derives from them.
1. Setup & First Story
Install, main.ts config, stories glob, and writing your first CSF3 story
# npx storybook@latest init # auto-detects Vite/Next.js/webpack + framework
# .storybook/main.ts — Storybook config:
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx|js|jsx)"],
addons: [
"@storybook/addon-essentials", // docs, controls, actions, viewport
"@storybook/addon-a11y", // accessibility checks
"@storybook/addon-interactions", // user interaction testing
"@chromatic-com/storybook", // visual regression (optional)
],
framework: {
name: "@storybook/react-vite", // or react-webpack5, nextjs, etc.
options: {},
},
docs: { autodocs: "tag" }, // auto-generate docs for tagged stories
};
export default config;
// src/components/Button/Button.stories.tsx — Component Story Format 3 (CSF3):
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
// Meta: component-level config
const meta: Meta<typeof Button> = {
component: Button,
title: "UI/Button", // sidebar path
tags: ["autodocs"], // auto-generate docs page
parameters: {
layout: "centered", // centered, fullscreen, padded
},
argTypes: {
variant: { control: "select", options: ["primary", "secondary", "danger"] },
size: { control: "radio", options: ["sm", "md", "lg"] },
onClick: { action: "clicked" }, // logs to Actions panel
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// Each named export = one story:
export const Primary: Story = {
args: { label: "Click me", variant: "primary", size: "md" },
};
export const Disabled: Story = {
args: { label: "Disabled", variant: "primary", disabled: true },
};
export const AllVariants: Story = {
render: () => (
<div style={{ display: "flex", gap: "1rem" }}>
<Button label="Primary" variant="primary" />
<Button label="Secondary" variant="secondary" />
<Button label="Danger" variant="danger" />
</div>
),
};
2. Args, Controls & Decorators
Dynamic args, argTypes controls, global decorators for providers, and parameters
// .storybook/preview.tsx — global config (applies to all stories):
import type { Preview } from "@storybook/react";
import { ThemeProvider } from "../src/providers/ThemeProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "../src/styles/globals.css";
const preview: Preview = {
decorators: [
// Wrap every story with providers:
(Story) => (
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>
<Story />
</ThemeProvider>
</QueryClientProvider>
),
],
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" }, // auto-instrument all onXxx props
controls: {
matchers: {
color: /(background|color)$/i, // show color picker for props ending in "color"
date: /Date$/i,
},
},
backgrounds: {
default: "light",
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#1a1a1a" },
],
},
},
};
export default preview;
// Story-level decorator — override for one story:
export const DarkTheme: Story = {
args: { label: "Dark button" },
decorators: [
(Story) => (
<div style={{ background: "#1a1a1a", padding: "2rem" }}>
<Story />
</div>
),
],
};
3. Interaction Testing with play()
Write interaction tests using play() — runs in browser and in Vitest CI
import { within, userEvent, expect } from "@storybook/test";
export const LoginFlow: Story = {
render: () => <LoginForm onSubmit={fn()} />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Find elements (same as Testing Library):
const emailInput = canvas.getByLabelText("Email");
const passwordInput = canvas.getByLabelText("Password");
const submitButton = canvas.getByRole("button", { name: "Sign in" });
// Interact:
await userEvent.type(emailInput, "alice@example.com");
await userEvent.type(passwordInput, "secret123");
await userEvent.click(submitButton);
// Assert:
await expect(canvas.getByText("Welcome, Alice!")).toBeInTheDocument();
await expect(submitButton).toBeDisabled();
},
};
// Loading state story:
export const SubmittingState: Story = {
args: { isSubmitting: true, label: "Sign in" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await expect(button).toBeDisabled();
await expect(canvas.getByRole("progressbar")).toBeInTheDocument();
},
};
// Run interaction tests in CI (Storybook 8 + Vitest):
// npx storybook test --watch // development mode
// npx storybook test // CI mode
// Or via vitest.config.ts with @storybook/experimental-addon-test
4. MSW Integration & Async Stories
Mock API calls with msw-storybook-addon, loaders for async data, and loading states
// npm install -D msw msw-storybook-addon
// npx msw init public/
// .storybook/preview.tsx:
import { initialize, mswLoader } from "msw-storybook-addon";
initialize({ onUnhandledRequest: "bypass" });
// Add MSW loader globally:
const preview: Preview = {
loaders: [mswLoader],
// ...
};
// In stories — mock API per-story:
import { http, HttpResponse } from "msw";
export const WithData: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/users", () =>
HttpResponse.json([
{ id: "1", name: "Alice", email: "alice@example.com" },
{ id: "2", name: "Bob", email: "bob@example.com" },
])
),
],
},
},
};
export const LoadingState: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/users", async () => {
await new Promise(resolve => setTimeout(resolve, 99999));
return HttpResponse.json([]);
}),
],
},
},
};
export const ErrorState: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/users", () => HttpResponse.json({ error: "Server error" }, { status: 500 })),
],
},
},
};
5. Docs, Accessibility & Visual Regression
Auto-generated docs with JSDoc, a11y checks, and Chromatic visual regression
// Auto-generated documentation with JSDoc + Storybook Docs:
/**
* Primary button for actions. Use `variant="danger"` for destructive actions.
* Supports all standard HTML button attributes.
*/
export const Button = ({
label,
variant = "primary",
size = "md",
/** Called when the button is clicked (not when disabled) */
onClick,
disabled = false,
}: ButtonProps) => <button ...>{label}</button>;
// The JSDoc comment appears automatically in the Args table.
// Add tags: ["autodocs"] to meta to generate a /docs route for the component.
// MDX docs page — custom documentation:
// Button.mdx:
// import { Canvas, Meta, Story, ArgTable } from "@storybook/blocks";
// <Meta of={ButtonStories} />
// # Button
// <Canvas of={ButtonStories.Primary} />
// <ArgTable of={ButtonStories} />
// Accessibility checks (addon-a11y):
// Stories automatically run axe-core accessibility tests in the "Accessibility" panel.
// Configure per-story:
export const AccessibleForm: Story = {
parameters: {
a11y: {
config: {
rules: [{ id: "color-contrast", enabled: true }],
},
},
},
};
// Visual regression with Chromatic:
// npx chromatic --project-token=YOUR_TOKEN
// CI: Chromatic compares screenshots per-story, shows diffs in PR.
// Free tier: 5,000 snapshots/month.
// Useful CLI commands:
// npx storybook dev -p 6006 — start dev server
// npx storybook build — build static site
// npx storybook test — run interaction tests
// npx storybook dev --docs — docs mode only
Track Node.js and frontend tooling releases at ReleaseRun. Related: Vitest Reference | React Reference | TypeScript Reference | React EOL Tracker
🔍 Free tool: npm Package Health Checker — check Storybook and its addon packages for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com