Skip to main content
Back to Blog
Tutorials
3 min read
November 22, 2024

How to Build a Component Library with Storybook

Set up Storybook for your React component library with stories, docs, interactions, accessibility testing, and publishing.

Ryel Banfield

Founder & Lead Developer

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.

Storybookcomponent librarydesign systemReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles