Skip to main content

Chapter 15: Component Documentation with Storybook

Storybook provides a dedicated environment for developing, testing, and documenting UI components in isolation. It gives you a visual catalog of every component, its variants, and its interactive states.

Setup for Vite

npx storybook@latest init

The init command detects your Vite + React setup and configures everything automatically:

  • .storybook/main.ts — Storybook configuration
  • .storybook/preview.ts — global decorators and parameters
  • Stories format: CSF (Component Story Format)

Configuration

// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx)"],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-a11y",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
typescript: {
reactDocgen: "react-docgen-typescript",
},
};

export default config;
// .storybook/preview.ts
import type { Preview } from "@storybook/react";
import "@/styles/app.css"; // Include your app styles

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
// Wrap stories in necessary providers
(Story) => (
<div className="p-4">
<Story />
</div>
),
],
};

export default preview;

Writing Stories with TypeScript

Basic Story

// shared/components/ui/button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button";

// Meta — describes the component
const meta = {
title: "UI/Button",
component: Button,
tags: ["autodocs"], // Generate documentation automatically
argTypes: {
variant: {
control: "select",
options: ["default", "destructive", "outline", "secondary", "ghost", "link"],
},
size: {
control: "select",
options: ["default", "sm", "lg", "icon"],
},
},
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// Stories — different states of the component
export const Default: Story = {
args: {
children: "Button",
},
};

export const Destructive: Story = {
args: {
variant: "destructive",
children: "Delete",
},
};

export const Outline: Story = {
args: {
variant: "outline",
children: "Outline",
},
};

export const Small: Story = {
args: {
size: "sm",
children: "Small",
},
};

export const Large: Story = {
args: {
size: "lg",
children: "Large Button",
},
};

export const Disabled: Story = {
args: {
children: "Disabled",
disabled: true,
},
};

export const Loading: Story = {
args: {
children: "Loading...",
disabled: true,
},
render: (args) => (
<Button {...args}>
<span className="mr-2 animate-spin"></span>
Loading...
</Button>
),
};

Complex Component Story

// features/projects/components/project-card.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { ProjectCard } from "./project-card";

const meta = {
title: "Features/Projects/ProjectCard",
component: ProjectCard,
tags: ["autodocs"],
decorators: [
(Story) => (
<div className="max-w-sm">
<Story />
</div>
),
],
} satisfies Meta<typeof ProjectCard>;

export default meta;
type Story = StoryObj<typeof meta>;

const baseProject = {
id: "proj-1",
name: "TaskForge",
description: "A project management application built with modern TypeScript tooling.",
status: "active" as const,
priority: "high" as const,
taskCount: 42,
completedTaskCount: 28,
updatedAt: new Date("2025-12-15"),
members: [
{ id: "u1", name: "Alice", avatarUrl: null },
{ id: "u2", name: "Bob", avatarUrl: null },
],
};

export const Active: Story = {
args: {
project: baseProject,
},
};

export const Archived: Story = {
args: {
project: { ...baseProject, status: "archived" },
},
};

export const LongDescription: Story = {
args: {
project: {
...baseProject,
description: "This is a very long description that should be truncated after a certain number of lines to prevent the card from becoming too tall and disrupting the grid layout.",
},
},
};

export const NoDescription: Story = {
args: {
project: { ...baseProject, description: null },
},
};

Interaction Testing in Stories

import { within, userEvent, expect } from "@storybook/test";

export const ClickInteraction: Story = {
args: {
children: "Click me",
onClick: fn(), // Storybook's spy function
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");

// Simulate user interaction
await userEvent.click(button);

// Verify the interaction
await expect(args.onClick).toHaveBeenCalledTimes(1);
},
};

Story Organization

Organize stories to mirror your component hierarchy:

Storybook Sidebar:
├── UI/ # shared/components/ui/
│ ├── Button
│ ├── Card
│ ├── Dialog
│ ├── Input
│ └── Select
├── Layout/ # shared/components/layout/
│ ├── PageHeader
│ ├── Sidebar
│ └── ContentArea
├── Feedback/ # shared/components/feedback/
│ ├── EmptyState
│ ├── ErrorBoundary
│ └── LoadingSpinner
└── Features/ # features/*/components/
├── Projects/
│ ├── ProjectCard
│ ├── ProjectList
│ └── ProjectForm
└── Tasks/
├── TaskCard
└── TaskList

Use the title field to control placement:

const meta = {
title: "Features/Projects/ProjectCard", // Folder/SubFolder/Component
// ...
} satisfies Meta<typeof ProjectCard>;

Running Storybook

# Development
pnpm storybook

# Build static site (for deployment)
pnpm build-storybook

Add to package.json:

{
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}
}

Summary

  • CSF format with Meta and StoryObj types for full TypeScript support
  • ✅ Use satisfies operator for strict type checking
  • Autodocs generates documentation from props and stories automatically
  • Interaction testing verifies component behavior within stories
  • Accessibility addon (@storybook/addon-a11y) runs axe-core checks in every story
  • ✅ Organize stories to mirror your component hierarchy
  • ✅ Include your app's CSS and providers in .storybook/preview.ts