storybook
Storybook stories, CSF3 format, autodocs. Use for component documentation.
$ Installer
git clone https://github.com/djimontyp/task-tracker /tmp/task-tracker && cp -r /tmp/task-tracker/.claude/skills/storybook ~/.claude/skills/task-tracker// tip: Run this command in your terminal to install the skill
SKILL.md
name: storybook description: Storybook stories, CSF3 format, autodocs. Use for component documentation.
Storybook Skill
Atomic Rule
Компонент в shared/ → story обов'язково в тому ж коміті.
F1 creates component → F1 creates story → Same commit
Before Creating UI
- Перевір Storybook на існуючі компоненти
- Використовуй існуючі замість створення нових
- Дотримуйся 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— forshared/ui/componentspattern.template.tsx— forshared/patterns/componentsfeature.template.tsx— forfeatures/*/components
References
- Gold standard:
frontend/src/shared/patterns/CardWithStatus.stories.tsx - Empty states:
frontend/src/shared/patterns/EmptyState.stories.tsx - CSF3 Format
- Autodocs
- Interaction Testing
- Play Functions
Repository

djimontyp
Author
djimontyp/task-tracker/.claude/skills/storybook
0
Stars
0
Forks
Updated4h ago
Added1w ago