All topics
Frontend · Learning hub

Storybook notes for developers

Master Storybook with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Storybook

Storybook Component Development

Storybook Component Development Writing Stories // Button.stories.tsx import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button';

Storybook Component Development

Writing Stories

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

// Meta — component-level config
const meta: Meta<typeof Button> = {
  title: 'UI/Button',          // sidebar grouping: UI > Button
  component: Button,
  tags: ['autodocs'],          // auto-generate docs page
  parameters: {
    layout: 'centered',        // center in canvas
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'danger'],
      description: 'Visual style of the button',
    },
    size: {
      control: { type: 'radio' },
      options: ['sm', 'md', 'lg'],
    },
    onClick: { action: 'clicked' },  // log action in Actions panel
    disabled: { control: 'boolean' },
  },
};
export default meta;
type Story = StoryObj<typeof Button>;

// Stories — individual states/variants
export const Primary: Story = {
  args: {
    children: 'Click me',
    variant: 'primary',
    size: 'md',
  },
};

export const Disabled: Story = {
  args: { ...Primary.args, disabled: true },
};

export const Loading: Story = {
  args: { ...Primary.args, isLoading: true },
  parameters: {
    docs: { description: { story: 'Shows spinner while loading' } },
  },
};

// Composing stories
export const AllVariants: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '8px' }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="danger">Danger</Button>
    </div>
  ),
};

Decorators, Mocking & Testing

// Global decorators — .storybook/preview.tsx
import type { Preview } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const preview: Preview = {
  decorators: [
    (Story) => (
      <QueryClientProvider client={new QueryClient()}>
        <div style={{ padding: '2rem' }}>
          <Story />
        </div>
      </QueryClientProvider>
    ),
  ],
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: { matchers: { color: /(background|color)$/i } },
  },
};
export default preview;

// Mock API calls — msw-storybook-addon
import { http, HttpResponse } from 'msw';

export const WithUser: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('/api/users/:id', ({ params }) =>
          HttpResponse.json({ id: params.id, name: 'Alice', role: 'admin' })
        ),
      ],
    },
  },
};

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

export const SubmitForm: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.type(canvas.getByLabelText('Email'), 'alice@example.com');
    await userEvent.click(canvas.getByRole('button', { name: 'Submit' }));
    await expect(canvas.getByText('Success!')).toBeInTheDocument();
  },
};

// Commands
// npx storybook dev -p 6006     — start dev server
// npx storybook build            — build static site
// npx storybook test             — run interaction tests
Storybook

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

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;
Storybook

Testing & Documentation

Testing & Documentation Storybook integrates interaction testing, accessibility auditing, and auto-generated docs. Stories double as tests — play functions simu

Testing & Documentation

Storybook integrates interaction testing, accessibility auditing, and auto-generated docs. Stories double as tests — play functions simulate user interactions, and the test runner asserts on outcomes.

Play Functions — Interaction Testing

// LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within, waitFor } from '@storybook/test';
import { LoginForm } from './LoginForm';

const meta: Meta<typeof LoginForm> = {
  component: LoginForm,
  args: { onSubmit: fn() },
};
export default meta;
type Story = StoryObj<typeof LoginForm>;

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

    // Interact with form elements
    await userEvent.type(
      canvas.getByLabelText('Email'),
      'alice@example.com',
      { delay: 50 }           // simulate real typing speed
    );
    await userEvent.type(
      canvas.getByLabelText('Password'),
      'password123'
    );
    await userEvent.click(
      canvas.getByRole('button', { name: /sign in/i })
    );

    // Assert
    await waitFor(() => expect(args.onSubmit).toHaveBeenCalledWith({
      email: 'alice@example.com',
      password: 'password123',
    }));
  },
};

export const ValidationErrors: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    // Submit without filling in the form
    await userEvent.click(canvas.getByRole('button', { name: /sign in/i }));
    // Expect validation messages
    await expect(canvas.getByText('Email is required')).toBeInTheDocument();
    await expect(canvas.getByText('Password is required')).toBeInTheDocument();
  },
};

Storybook Test Runner & CI

# Install test runner
npm install --save-dev @storybook/test-runner

# Run all play functions as tests
npx storybook test

# Run specific story
npx storybook test --stories="**/Button.stories.*"

# With coverage
npx storybook test --coverage

# In CI — start storybook, then test
# package.json scripts:
# test:storybook: storybook dev -p 6006 --ci & wait-on tcp:6006 && storybook test

# Or use concurrently
# concurrently -k -s=first 'storybook dev -p 6006 --ci' 'wait-on tcp:6006 && storybook test'

Accessibility Testing

// @storybook/addon-a11y — runs axe-core on every story
// Install: npm install --save-dev @storybook/addon-a11y
// Add to .storybook/main.ts addons array: '@storybook/addon-a11y'

// Configure per story
export const InaccessibleButton: Story = {
  args: { children: 'Click', 'aria-label': undefined },
  parameters: {
    a11y: {
      // Override axe rules
      config: {
        rules: [
          { id: 'button-name', enabled: true },
          { id: 'color-contrast', enabled: true },
        ],
      },
      // Expected violations (use sparingly)
      options: { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] } },
    },
  },
};

// Disable a11y check for a known issue
export const KnownIssue: Story = {
  parameters: {
    a11y: { disable: true },
  },
};

// Run a11y tests in CI
// storybook test --includeStories='.*'
// All a11y violations will fail the test run

MSW API Mocking & Mock Providers

// Mock Service Worker in Storybook — msw-storybook-addon
// Install: npm install --save-dev msw msw-storybook-addon

// .storybook/preview.tsx — initialize MSW
import { initialize, mswLoader } from 'msw-storybook-addon';
initialize({ onUnhandledRequest: 'bypass' });

const preview: Preview = {
  loaders: [mswLoader],
  // ...
};

// UserProfile.stories.tsx — mock API per story
import { http, HttpResponse, delay } from 'msw';

export const LoadedProfile: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('/api/users/:id', async ({ params }) => {
          await delay(300);           // simulate network latency
          return HttpResponse.json({
            id: params.id,
            name: 'Alice Smith',
            email: 'alice@example.com',
            role: 'admin',
            avatar: 'https://i.pravatar.cc/150?u=alice',
          });
        }),
      ],
    },
  },
};

export const ErrorState: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('/api/users/:id', () =>
          HttpResponse.json({ message: 'Not found' }, { status: 404 })
        ),
      ],
    },
  },
};

Keep your Storybook knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever