Back to skills
extension
Category: Development & EngineeringNo API key required

storybook

Storybook stories, CSF3 format, autodocs. Use for component documentation.

personAuthor: jakexiaohubgithub

Storybook Skill

Atomic Rule

Компонент в shared/ → story обов'язково в тому ж коміті.

F1 creates component → F1 creates story → Same commit

Before Creating UI

  1. Перевір Storybook на існуючі компоненти
  2. Використовуй існуючі замість створення нових
  3. Дотримуйся patterns з Design System
just storybook  # http://localhost:6006

Story Requirements

CSF3 Format

import type { Meta, StoryObj } from '@storybook/react-vite';
import { ComponentName } from './ComponentName';

const meta: Meta<typeof ComponentName> = {
  title: 'Category/ComponentName',  // See naming below
  component: ComponentName,
  tags: ['autodocs'],               // REQUIRED!
  parameters: {
    docs: {
      description: {
        component: 'Brief description of the component purpose.',
      },
    },
  },
};

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

export const Default: Story = {
  args: {
    // Default props
  },
};

Naming Convention

| Location | Title Prefix | Example | |----------|--------------|---------| | shared/ui/ | UI/ | UI/Button, UI/Card | | shared/patterns/ | Design System/Patterns/ | Design System/Patterns/CardWithStatus | | shared/components/ | Components/ | Components/DataTable | | features/*/components/ | Features/{Domain}/ | Features/Analysis/RunCard |

Minimum Stories by Tier

| Tier | Location | Minimum Stories | |------|----------|-----------------| | 1 | shared/ui/ | 4-6 (Default, Variants, States, Sizes) | | 2 | shared/patterns/ | 5-8 (All statuses, compositions, use cases) | | 3 | features/ | 2-4 (Default, Empty, Loading, Error) |

Required Story Types

For Shared UI (Tier 1)

// REQUIRED
export const Default: Story = { args: { ... } };

// If component has variants
export const Primary: Story = { args: { variant: 'primary' } };
export const Secondary: Story = { args: { variant: 'secondary' } };
export const Destructive: Story = { args: { variant: 'destructive' } };

// If component has sizes
export const Small: Story = { args: { size: 'sm' } };
export const Large: Story = { args: { size: 'lg' } };

// If component has states
export const Disabled: Story = { args: { disabled: true } };
export const Loading: Story = { args: { loading: true } };

For Patterns (Tier 2)

// REQUIRED - All status states
export const Connected: Story = { args: { status: 'connected' } };
export const Validating: Story = { args: { status: 'validating' } };
export const Pending: Story = { args: { status: 'pending' } };
export const Error: Story = { args: { status: 'error' } };

// REQUIRED - Compositions
export const WithFooter: Story = { args: { footer: <Button>Action</Button> } };
export const WithContent: Story = { args: { children: <div>...</div> } };

// REQUIRED - Layout examples
export const CardGrid: Story = {
  render: () => (
    <div className="grid grid-cols-3 gap-4">
      <Component ... />
      <Component ... />
      <Component ... />
    </div>
  ),
};

For Feature Components (Tier 3)

// REQUIRED
export const Default: Story = { args: { ... } };
export const Empty: Story = { args: { items: [] } };
export const Loading: Story = { args: { isLoading: true } };
export const Error: Story = { args: { error: new Error('Failed to load') } };

Providers for Stories

| Hook Used | Required Provider | |-----------|-------------------| | useTheme | ThemeProvider | | useLocation, Link | MemoryRouter | | useQuery | QueryClientProvider | | useSidebar | SidebarProvider |

Decorator Template

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@/shared/components/ThemeProvider';

const queryClient = new QueryClient();

const meta: Meta<typeof Component> = {
  component: Component,
  decorators: [
    (Story) => (
      <QueryClientProvider client={queryClient}>
        <ThemeProvider>
          <MemoryRouter>
            <Story />
          </MemoryRouter>
        </ThemeProvider>
      </QueryClientProvider>
    ),
  ],
};

When Story Is Mandatory

| Location | Mandatory? | Notes | |----------|------------|-------| | shared/ui/ | ✅ Yes | All UI primitives | | shared/patterns/ | ✅ Yes | All patterns | | shared/components/ | ✅ Yes | If exported publicly | | features/*/components/ | ⚠️ Conditional | If >50 LOC or reused | | pages/ | ❌ No | Use E2E tests |

Output Format

When creating component + story:

✅ Component implemented

Component: [Name]
Files:
- [component path]
- [story path]
Story: [Tier] / [# of stories]
Verify: npm run build && npx tsc --noEmit

Interaction Testing

Use play functions to test user interactions directly in stories.

Basic Interaction Test

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

export const ClickTest: Story = {
  args: {
    onClick: fn(),  // Mock function from @storybook/test
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);

    // Find and click button
    const button = canvas.getByRole('button');
    await userEvent.click(button);

    // Assert callback was called
    await expect(args.onClick).toHaveBeenCalled();
  },
};

Form Interaction Test

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

    // Fill form
    await userEvent.type(canvas.getByLabelText('Email'), 'test@example.com');
    await userEvent.type(canvas.getByLabelText('Password'), 'password123');

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

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

Dialog/Modal Test

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

    // Open dialog
    await userEvent.click(canvas.getByRole('button', { name: /open/i }));

    // Wait for dialog
    await expect(canvas.findByRole('dialog')).resolves.toBeInTheDocument();

    // Close with ESC
    await userEvent.keyboard('{Escape}');
    await expect(canvas.queryByRole('dialog')).not.toBeInTheDocument();
  },
};

Keyboard Navigation Test

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

    // Tab through elements
    await userEvent.tab();
    await expect(canvas.getByRole('button', { name: /first/i })).toHaveFocus();

    await userEvent.tab();
    await expect(canvas.getByRole('button', { name: /second/i })).toHaveFocus();

    // Activate with Enter
    await userEvent.keyboard('{Enter}');
  },
};

When to Add Interaction Tests

| Component Type | Interaction Test Required? | |----------------|---------------------------| | Buttons with onClick | ✅ Yes | | Forms | ✅ Yes | | Dialogs/Modals | ✅ Yes | | Dropdowns | ✅ Yes | | Static display | ❌ No | | Layout components | ❌ No |

Running Interaction Tests

# In Storybook UI - click "Interactions" tab
just storybook

# CLI (requires Storybook running)
just storybook-test

# CI mode
just storybook-test-ci

Templates

Use templates from @templates/ directory:

  • shared-ui.template.tsx — for shared/ui/ components
  • pattern.template.tsx — for shared/patterns/ components
  • feature.template.tsx — for features/*/ components

References