Storybook
01 / 03

Storybook Component Development

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

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

Start free