Storybook
02 / 03

Storybook Setup & Stories

Storybook Setup & Stories

Storybook is an isolated UI development environment. Stories are the unit of work — each story renders a component in a specific state. Use CSF (Component Story Format) for interoperable, testable stories.

Installation & Configuration

# Initialize Storybook in an existing project
npx storybook@latest init

# Detects framework automatically (React, Vue, Svelte, Angular, etc.)
# Creates .storybook/main.ts and .storybook/preview.ts
# Adds example stories in src/stories/

# Dev server
npx storybook dev -p 6006

# Production static build
npx storybook build
npx http-server storybook-static    # serve locally

# Key addons
npm install --save-dev
  @storybook/addon-essentials       # controls, actions, docs, viewport, backgrounds
  @storybook/addon-a11y             # accessibility auditing
  @storybook/addon-interactions     # interaction testing
  @storybook/test                   # play functions
  msw-storybook-addon               # API mocking with MSW
  storybook-addon-themes            # theme switching

.storybook/main.ts — Framework Config

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

const config: StorybookConfig = {
  // Story file patterns — where to find stories
  stories: [
    '../src/**/*.mdx',
    '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
  ],

  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@chromatic-com/storybook',       // visual regression testing
  ],

  framework: {
    name: '@storybook/react-vite',    // use Vite builder
    options: {},
  },

  // Expose env variables to stories
  env: (config) => ({
    ...config,
    STORYBOOK_API_URL: process.env.STORYBOOK_API_URL ?? 'http://localhost:3000',
  }),

  // Static files served at /public
  staticDirs: ['../public'],

  // TypeScript config
  typescript: {
    check: false,
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => !prop.parent?.fileName.includes('node_modules'),
    },
  },
};
export default config;

Writing Stories — CSF Format

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

// Meta — component-level config
const meta: Meta<typeof Button> = {
  title: 'UI/Button',                 // sidebar group: UI > Button
  component: Button,
  tags: ['autodocs'],                 // auto-generate docs page from JSDoc + props
  parameters: {
    layout: 'centered',               // 'centered' | 'fullscreen' | 'padded'
    docs: {
      description: {
        component: 'Primary UI button. Use for main actions.',
      },
    },
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'danger', 'ghost'],
      description: 'Visual style',
      table: { defaultValue: { summary: 'primary' } },
    },
    size: {
      control: { type: 'radio' },
      options: ['sm', 'md', 'lg'],
    },
    disabled: { control: 'boolean' },
    isLoading: { control: 'boolean' },
    onClick: { action: 'clicked' },   // log to Actions panel
  },
  args: {
    // Default args for all stories in this file
    onClick: fn(),                    // spy function (Storybook 8+)
    variant: 'primary',
    size: 'md',
  },
};
export default meta;
type Story = StoryObj<typeof Button>;

// Stories
export const Primary: Story = {
  args: { children: 'Click me' },
};

export const Danger: Story = {
  args: { children: 'Delete', variant: 'danger' },
};

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

export const Loading: Story = {
  args: { children: 'Saving...', isLoading: true },
};

// Custom render function
export const AllVariants: Story = {
  render: (args) => (
    <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
      {(['primary', 'secondary', 'danger', 'ghost'] as const).map((v) => (
        <Button key={v} {...args} variant={v}>{v}</Button>
      ))}
    </div>
  ),
};

Global Decorators & Preview Config

// .storybook/preview.tsx — global config applied to all stories
import type { Preview } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from '../src/providers/ThemeProvider';
import '../src/styles/globals.css';

const preview: Preview = {
  decorators: [
    // Wrap all stories in required providers
    (Story) => (
      <QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
        <BrowserRouter>
          <ThemeProvider defaultTheme="light">
            <div style={{ fontFamily: 'Inter, sans-serif', padding: '1rem' }}>
              <Story />
            </div>
          </ThemeProvider>
        </BrowserRouter>
      </QueryClientProvider>
    ),
  ],

  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    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' },
      ],
    },
    viewport: {
      viewports: {
        mobile: { name: 'Mobile', styles: { width: '375px', height: '812px' } },
        tablet: { name: 'Tablet', styles: { width: '768px', height: '1024px' } },
        desktop: { name: 'Desktop', styles: { width: '1440px', height: '900px' } },
      },
    },
  },
};
export default preview;

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free