Storybook lets you develop, test, and document UI components in isolation. Here is how to set it up.
Step 1: Install Storybook
npx storybook@latest init
This automatically detects your framework and installs the right packages.
Step 2: Configure for Tailwind CSS
// .storybook/preview.ts
import type { Preview } from "@storybook/react";
import "../src/app/globals.css"; // Import your global styles
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: "light",
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#0f172a" },
{ name: "gray", value: "#f1f5f9" },
],
},
layout: "centered",
},
decorators: [
(Story) => (
<div className="font-sans">
<Story />
</div>
),
],
};
export default preview;
Step 3: Write Button Stories
// src/components/ui/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Button } from "./Button";
const meta = {
title: "UI/Button",
component: Button,
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["default", "secondary", "outline", "ghost", "destructive"],
description: "Visual style of the button",
},
size: {
control: "select",
options: ["sm", "default", "lg", "icon"],
description: "Size of the button",
},
disabled: {
control: "boolean",
},
asChild: {
table: { disable: true },
},
},
args: {
onClick: fn(),
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: "Button",
variant: "default",
size: "default",
},
};
export const Secondary: Story = {
args: {
children: "Secondary",
variant: "secondary",
},
};
export const Outline: Story = {
args: {
children: "Outline",
variant: "outline",
},
};
export const Ghost: Story = {
args: {
children: "Ghost",
variant: "ghost",
},
};
export const Destructive: Story = {
args: {
children: "Delete",
variant: "destructive",
},
};
export const Small: Story = {
args: {
children: "Small",
size: "sm",
},
};
export const Large: Story = {
args: {
children: "Large",
size: "lg",
},
};
export const Disabled: Story = {
args: {
children: "Disabled",
disabled: true,
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-3">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
</div>
),
};
Step 4: Card Component Story
// src/components/ui/Card.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "./Card";
import { Button } from "./Button";
const meta = {
title: "UI/Card",
component: Card,
tags: ["autodocs"],
parameters: {
layout: "padded",
},
} satisfies Meta<typeof Card>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<Card className="w-[380px]">
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>
Card description goes here with supporting text.
</CardDescription>
</CardHeader>
<CardContent>
<p>Card content area. Put any content here.</p>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</CardFooter>
</Card>
),
};
export const PricingCard: Story = {
render: () => (
<Card className="w-[320px]">
<CardHeader>
<CardTitle>Pro Plan</CardTitle>
<CardDescription>For growing teams</CardDescription>
<p className="text-3xl font-bold">
$29<span className="text-sm font-normal text-gray-500">/month</span>
</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm">
<li>Unlimited projects</li>
<li>10 team members</li>
<li>50 GB storage</li>
<li>Priority support</li>
</ul>
</CardContent>
<CardFooter>
<Button className="w-full">Get Started</Button>
</CardFooter>
</Card>
),
};
Step 5: Interaction Tests
// src/components/ui/Dialog.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within, fn } from "@storybook/test";
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle } from "./Dialog";
import { Button } from "./Button";
const meta = {
title: "UI/Dialog",
component: Dialog,
tags: ["autodocs"],
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
</DialogHeader>
<p>Dialog content goes here.</p>
</DialogContent>
</Dialog>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Click the trigger
await userEvent.click(canvas.getByText("Open Dialog"));
// Verify dialog opened
const dialog = within(document.body);
await expect(dialog.getByText("Dialog Title")).toBeVisible();
},
};
Step 6: Accessibility Addon
pnpm add -D @storybook/addon-a11y
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx)"],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-a11y",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
};
export default config;
This adds an accessibility panel to every story that checks for WCAG violations.
Step 7: Package.json Scripts
{
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test-storybook": "test-storybook"
}
}
Step 8: Deploy to Chromatic
pnpm add -D chromatic
# .github/workflows/chromatic.yml
name: Chromatic
on: push
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
Summary
- Stories document each component state and variant
- Autodocs generate documentation from stories and TypeScript props
- Interaction tests verify component behavior
- Accessibility addon catches a11y issues during development
- Chromatic provides visual regression testing and hosted documentation
Need a Design System?
We build scalable component libraries and design systems for product teams. Contact us to discuss your project.