storybook

Installation
SKILL.md

Storybook

Build and document UI components in isolation with interactive stories.

Instructions

  1. Write stories for all components - Each component should have a story file
  2. Use args for props - Define props through args for interactive controls
  3. Add decorators - Wrap stories with providers and layout wrappers
  4. Document with MDX - Write component documentation alongside stories
  5. Test interactions - Use play functions for interaction testing

Setup

# Initialize Storybook in existing project
npx storybook@latest init

# Start Storybook
npm run storybook

Configuration

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

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};

export default config;

Writing Stories

Basic Story Structure

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  parameters: {
    layout: 'centered',
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'danger', 'ghost'],
      description: 'The visual style of the button',
    },
    size: {
      control: 'radio',
      options: ['sm', 'md', 'lg'],
    },
    disabled: {
      control: 'boolean',
    },
    onClick: { action: 'clicked' },
  },
};

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

export const Primary: Story = {
  args: {
    children: 'Primary Button',
    variant: 'primary',
  },
};

export const Secondary: Story = {
  args: {
    children: 'Secondary Button',
    variant: 'secondary',
  },
};

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

Multiple Variants

export const AllVariants: Story = {
  render: () => (
    <div className="flex flex-wrap gap-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="danger">Danger</Button>
      <Button variant="ghost">Ghost</Button>
    </div>
  ),
};

export const AllSizes: Story = {
  render: () => (
    <div className="flex items-center gap-4">
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
};

With Args

// args allow interactive control in Storybook UI
export const Playground: Story = {
  args: {
    children: 'Click me',
    variant: 'primary',
    size: 'md',
    disabled: false,
    loading: false,
  },
};

Decorators

Global Decorators

// .storybook/preview.tsx
import type { Preview } from '@storybook/react';
import { ThemeProvider } from '../src/providers/ThemeProvider';
import '../src/styles/globals.css';

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

export default preview;

Story-Level Decorators

export const WithDarkBackground: Story = {
  decorators: [
    (Story) => (
      <div className="bg-gray-900 p-8">
        <Story />
      </div>
    ),
  ],
  args: {
    children: 'Dark Mode Button',
    variant: 'primary',
  },
};

Interaction Testing

Play Functions

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

export const ClickInteraction: Story = {
  args: {
    children: 'Click me',
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: /click me/i });

    await userEvent.click(button);

    // Assertions
    await expect(button).toBeVisible();
  },
};

export const FormSubmission: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // Fill form fields
    await userEvent.type(
      canvas.getByLabelText(/email/i),
      'user@example.com'
    );
    await userEvent.type(
      canvas.getByLabelText(/password/i),
      'password123'
    );

    // Submit form
    await userEvent.click(canvas.getByRole('button', { name: /submit/i }));

    // Check result
    await expect(canvas.getByText(/success/i)).toBeInTheDocument();
  },
};

Component Documentation

MDX Documentation

{/* Button.mdx */}
import { Meta, Story, Canvas, Controls, Source } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';

<Meta of={ButtonStories} />

# Button

Buttons trigger actions or navigation. Use the appropriate variant
for the action's importance and context.

## Usage Guidelines

- Use **Primary** for the main action on a page
- Use **Secondary** for less important actions
- Use **Danger** only for destructive actions
- Limit to one primary button per section

## Examples

### Default

<Canvas of={ButtonStories.Primary} />

### All Variants

<Canvas of={ButtonStories.AllVariants} />

## Props

<Controls />

## Accessibility

- Buttons have proper focus states
- Loading state disables interaction and shows spinner
- Use `aria-label` for icon-only buttons

## Code

<Source of={ButtonStories.Primary} />

Inline Stories in MDX

import { Canvas, Meta } from '@storybook/blocks';
import { Button } from './Button';

<Meta title="Guide/Button Usage" />

# Using Buttons

Here's how to use buttons in different contexts:

<Canvas>
  <Button variant="primary">Save Changes</Button>
  <Button variant="secondary">Cancel</Button>
</Canvas>

Parameters

Layout

export const Centered: Story = {
  parameters: {
    layout: 'centered', // centered | fullscreen | padded
  },
};

Backgrounds

export const OnDark: Story = {
  parameters: {
    backgrounds: {
      default: 'dark',
    },
  },
};

Viewport

export const Mobile: Story = {
  parameters: {
    viewport: {
      defaultViewport: 'mobile1',
    },
  },
};

Addons

Accessibility Addon

// Automatically checks accessibility
export const AccessibleButton: Story = {
  args: {
    children: 'Accessible',
    'aria-label': 'Accessible button description',
  },
  parameters: {
    a11y: {
      config: {
        rules: [
          { id: 'color-contrast', enabled: true },
        ],
      },
    },
  },
};

Actions Addon

const meta: Meta<typeof Button> = {
  component: Button,
  argTypes: {
    onClick: { action: 'clicked' },
    onFocus: { action: 'focused' },
    onBlur: { action: 'blurred' },
  },
};

Project Structure

src/
├── components/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.test.tsx
│   │   ├── Button.stories.tsx
│   │   ├── Button.mdx
│   │   └── index.ts
│   ├── Card/
│   │   ├── Card.tsx
│   │   ├── Card.stories.tsx
│   │   └── index.ts
│   └── index.ts
├── .storybook/
│   ├── main.ts
│   └── preview.tsx
└── package.json

Best Practices

Practice Recommendation
File naming Component.stories.tsx alongside component
Story naming Use descriptive names like WithIcon, Disabled
Args Prefer args over inline JSX for flexibility
Decorators Use for providers, layout, theming
Play functions Test interactions, not just rendering
Documentation Write MDX docs for complex components

When to Use

  • Component library development
  • Design system documentation
  • Visual regression testing
  • Isolated component development
  • Team collaboration on UI
  • Accessibility testing

Notes

  • Storybook 7+ uses Component Story Format (CSF) 3
  • Works with React, Vue, Angular, Svelte, and more
  • Integrates with Chromatic for visual testing
  • Supports TypeScript out of the box
  • Hot module reloading for fast development
Related skills

More from housegarofalo/claude-code-base

Installs
4
GitHub Stars
2
First Seen
Mar 15, 2026