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 — for shared/ui/ components
pattern.template.tsx — for shared/patterns/ components
feature.template.tsx — for features/*/ components
References