Back to docs
12Week 2

Storybook

Build, document, and share your living component library

What is Storybook?

Storybook is an open-source tool for developing UI components in isolation. It runs alongside your app — not inside it — and gives you a dedicated sandbox where you can render every component in every state without needing to navigate your actual application.

Think of it as a living component library: every component has a page, every state has a preview, and designers and developers can browse, test, and review the entire UI without running the full app.

For design systems, Storybook is essential. It is the place where your token system, components, and documentation come together into a single, browsable reference.

Installation

Storybook has a one-command setup that detects your framework and configures everything automatically.

npx storybook@latest init

This will detect that you are using Next.js and install the correct dependencies. Once complete, start Storybook with:

npm run storybook

Storybook will open at http://localhost:6006 in your browser.

Warning: You may see peer dependency warnings during installation. This is normal with Next.js projects. If Storybook starts and renders your stories, you can safely ignore these warnings. If it fails to start, try deleting node_modules and running npm install again.

Connecting Tailwind to Storybook

By default, Storybook does not load your Tailwind styles. You need to import your global CSS in the Storybook preview configuration.

// .storybook/preview.ts
import type { Preview } from "@storybook/react"
import "../app/globals.css"

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
}

export default preview

The key line is import "../app/globals.css" — this loads your Tailwind base styles, your custom CSS variables, and your brand token definitions into Storybook.

Writing Stories

A story is a single state of a component. Each component gets a .stories.tsx file that lives next to it. Stories define the different ways a component can be rendered.

Basic Story Structure

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

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

export default meta
type Story = StoryObj<typeof Button>

// Individual stories
export const Default: Story = {
  args: {
    children: "Button",
    variant: "default",
  },
}

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

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

export const Secondary: Story = {
  args: {
    children: "Secondary",
    variant: "secondary",
  },
}

export const Ghost: Story = {
  args: {
    children: "Ghost",
    variant: "ghost",
  },
}

export const Link: Story = {
  args: {
    children: "Link",
    variant: "link",
  },
}

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

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

// Composite story showing all variants
export const AllVariants: Story = {
  render: () => (
    <div className="flex flex-wrap gap-4">
      <Button variant="default">Default</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
    </div>
  ),
}

Story for Inputs with States

// components/ui/Input.stories.tsx
import type { Meta, StoryObj } from "@storybook/react"
import { Input } from "./input"

const meta: Meta<typeof Input> = {
  title: "UI/Input",
  component: Input,
  tags: ["autodocs"],
}

export default meta
type Story = StoryObj<typeof Input>

export const Default: Story = {
  args: {
    placeholder: "Enter your email...",
  },
}

export const WithValue: Story = {
  args: {
    value: "hello@example.com",
    readOnly: true,
  },
}

export const Disabled: Story = {
  args: {
    placeholder: "Disabled input",
    disabled: true,
  },
}

export const WithLabel: Story = {
  render: () => (
    <div className="space-y-2">
      <label htmlFor="email" className="text-sm font-medium">
        Email
      </label>
      <Input id="email" type="email" placeholder="hello@example.com" />
    </div>
  ),
}

export const Error: Story = {
  render: () => (
    <div className="space-y-2">
      <label htmlFor="email-error" className="text-sm font-medium">
        Email
      </label>
      <Input
        id="email-error"
        type="email"
        placeholder="hello@example.com"
        className="border-red-500 focus-visible:ring-red-500"
      />
      <p className="text-sm text-red-500">Please enter a valid email address.</p>
    </div>
  ),
}

Organizing Stories

The title field in your meta object controls where the story appears in the Storybook sidebar. Use slashes to create a hierarchy.

// Flat
title: "Button"

// Grouped under "UI"
title: "UI/Button"

// Nested deeper
title: "UI/Forms/Input"
title: "UI/Forms/Select"
title: "UI/Forms/Textarea"

// Separate section for patterns
title: "Patterns/Card Grid"
title: "Patterns/Hero Section"

// Brand-specific
title: "Brands/Midnight/Button"
title: "Brands/Ocean/Button"

Decorators — Wrapping Stories

Decorators let you wrap every story with a provider, layout, or context. This is essential for multi-brand design systems — you can wrap stories with your BrandProvider to preview components in each brand.

// .storybook/preview.ts
import type { Preview } from "@storybook/react"
import { BrandProvider } from "../components/brand-provider"
import "../app/globals.css"

const preview: Preview = {
  decorators: [
    (Story) => (
      <BrandProvider defaultBrand="midnight">
        <div className="p-8">
          <Story />
        </div>
      </BrandProvider>
    ),
  ],
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
}

export default preview

You can also add decorators to individual stories or story files:

// In a specific story file
const meta: Meta<typeof Button> = {
  title: "UI/Button",
  component: Button,
  decorators: [
    (Story) => (
      <div className="bg-gray-100 p-8 rounded-lg">
        <Story />
      </div>
    ),
  ],
}

Deploying Storybook

A deployed Storybook gives your entire team — designers, developers, PMs — a shared reference for every component. There are two main options.

Option A — Vercel

Build Storybook as a static site and deploy it to Vercel.

  • Add a build script to package.json:
    "build-storybook": "storybook build"
  • Run npm run build-storybook to generate a storybook-static folder.
  • Deploy the storybook-static folder to Vercel as a separate project, or configure it as a subdirectory deployment.

Option B — Chromatic

Chromatic is a cloud service built by the Storybook team. It provides visual regression testing, review workflows, and automatic publishing.

  • Sign up at chromatic.com and connect your repository.
  • Install Chromatic:
    npm install --save-dev chromatic
  • Publish:
    npx chromatic --project-token=YOUR_TOKEN
  • Chromatic will build your Storybook, take screenshots of every story, and host the result. On every pull request, it compares screenshots and flags visual changes for review.