~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted Story work.
Story
- Component development snapshots for isolated testing
- Interactive documentation of UI component states
- Enables visual regression testing and design review
Key Insight
A Storybook story is a component development snapshot—capturing one specific visual state with precise props and data. Stories transform your component library into an interactive workshop where designers, developers, and QA can explore every edge case (loading state, error state, empty data) without touching application code. Think of stories as unit tests for the eyes: each story isolates and documents exactly how your component should look and behave in different scenarios.
Detailed Description
Stories are the fundamental building blocks of Storybook, providing an isolated environment to develop and test UI components independently from the rest of your application.
What is a Story?
A story is a named export in a .stories.js or .stories.ts file that describes how to render a component with specific props, state, and data. Each story represents one “interesting state” of your component—such as a button in its disabled state, a form with validation errors, or a data table with 1000 rows.
Component Story Format (CSF): Storybook uses CSF (Component Story Format), an ES6 module standard that makes stories portable and machine-readable:
- Default export: Metadata about the component (title, component, decorators, etc.)
- Named exports: Individual stories, each representing a specific component state
- Args: Props passed to the component, editable via Controls addon
- Decorators: Wrapper components that provide context (themes, routers, providers)
- Play functions: Automated interactions to test user flows
Benefits of Story-Driven Development:
- Isolated Development: Build components without starting the entire app
- Visual Testing: Catch UI regressions by comparing story snapshots
- Living Documentation: Stories show designers/stakeholders actual component behavior
- Edge Case Coverage: Explicitly test loading states, errors, empty data, long text
- Team Collaboration: Shared workspace for developers, designers, and QA
- Faster Debugging: Reproduce bugs by creating a story that matches production state
Storybook Workflow: Developers write stories alongside components → Storybook renders stories in isolated iframe → Addons provide Controls (edit args), Actions (event logging), Accessibility checks, Visual regression tests → Stories become reusable test fixtures for unit tests, integration tests, and screenshot tests.
By leveraging stories, developers can efficiently build, document, and test UI components, making Storybook a powerful tool in the frontend development process.
The example below shows the same Button atom rewritten as a story across all four template families — so you can see exactly where CSF stays the same and where each framework adapter pulls its own weight.
Code Examples
Basic Example: Button stories across Storybook flavours
Storybook 10 standardises on Component Story Format (CSF 3) — one default export with title and component, plus named exports per story. The framework wrapper (@storybook/react-vite / @storybook/angular / @storybook/vue3-vite / @storybook/web-components-vite) decides how args get plugged into your component. Same Button, four CSF dialects.
React
// templates/chota-react-redux/src/ui/atoms/Button/Button.stories.js
// CSF 3 + react-vite. `args` map directly onto the component's props.
import Button from "./Button.component";
export default { title: "Atoms/Button", component: Button };
export const Default = {
args: {
className: "button primary",
children: "Sample Button",
},
};
export const Loading = {
args: {
isLoading: true,
className: "button primary",
children: "Sample Button",
},
};
Angular
// templates/chota-angular-ngrx/src/ui/atoms/Button/Button.stories.ts
// Angular stories pair `args` with a small `render` function that maps
// them into a component template (since args don't auto-bind onto Inputs).
import type { Meta, StoryObj } from '@storybook/angular';
import ButtonComponent from './Button.component';
type Story = StoryObj<ButtonComponent>;
export default {
title: 'Atoms/Button',
component: ButtonComponent,
} satisfies Meta<ButtonComponent>;
export const Default: Story = {
args: { classes: 'button primary', isLoading: false },
render: (args) => ({
props: args,
template: `<app-button [classes]="classes" [isLoading]="isLoading">Sample Button</app-button>`,
}),
};
export const Loading: Story = {
args: { isLoading: true, classes: 'button primary' },
render: (args) => ({
props: args,
template: `<app-button [classes]="classes" [isLoading]="isLoading">Sample Button</app-button>`,
}),
};
Vue
// templates/chota-vue-pinia/src/ui/atoms/Button/Button.stories.js
// Vue's CSF uses a Template factory that returns a component definition.
// Each story binds args via `Template.bind({})` — the classic CSF 2 form,
// still supported in Storybook 10 alongside CSF 3.
import { defineComponent } from 'vue';
import Button from "./Button.component.vue";
const Template = (args) => defineComponent({
components: { Button },
setup: () => ({ args }),
template: '<Button @onClick="args.onClick" :class="args.class" :isLoading="args.isLoading">{{ args.label }}</Button>',
});
export default { title: "Atoms/Button", component: Button };
export const Default = Template.bind({});
Default.args = {
class: "button primary",
label: "Sample Button",
onClick: () => console.log('working on click'),
};
export const Loading = Template.bind({});
Loading.args = {
isLoading: true,
class: "button primary",
label: "Sample Button",
};
Web Components
// templates/chota-wc-saga/src/ui/atoms/Button/Button.stories.js
// web-components-vite stories use Lit-html in the render function.
// Note `.classes=` for property binding and `@onClick=` for the
// CustomEvent emitted by the Button atom.
import { html } from 'lit';
import './app-button';
export default {
title: 'Atoms/Button',
render: (args) => html`
<app-button .classes="${args.classes}" .isLoading="${args.isLoading}"
@onClick="${args.onClick}">
${args.label}
</app-button>
`,
};
export const Default = {
args: {
classes: 'button primary',
label: 'Sample Button',
onClick: (e) => console.log('click', e.detail),
},
};
export const Loading = {
args: {
isLoading: true,
classes: 'button primary',
label: 'Sample Button',
},
};
The CSF meta-shape (default export with title/component + named story exports with args) is identical across all four. Where they diverge:
- React + Web Components can let
argsflow straight through to the component (React via prop spread, WC via Lit’s render template referencing args by name). - Angular needs an explicit
renderfunction returning{ props, template }because Angular Inputs don’t auto-bind from a generic prop bag. - Vue still uses the classic
Template.bind({})factory pattern in the shipped templates, though CSF 3 with object args also works fine.
Storybook addons (addon-docs, addon-a11y, addon-controls) all read these stories the same way regardless of framework.
Practical Example: Form Component with Decorators and Play Functions
// LoginForm.stories.jsx - Advanced story features
import React from 'react';
// Storybook 10 ships interaction helpers from a single package:
import { within, userEvent, expect } from '@storybook/test';
import { LoginForm } from './LoginForm';
export default {
title: 'Forms/LoginForm',
component: LoginForm,
// Decorators: Add theme/router/store context
decorators: [
(Story) => (
<div style={{ maxWidth: '400px', margin: '50px auto' }}>
<Story />
</div>
)
],
// Global args shared by all stories
args: {
onSubmit: () => console.log('Form submitted')
}
};
// Story 1: Empty form (initial state)
export const Empty = {
args: {}
};
// Story 2: Filled form
export const Filled = {
args: {
initialValues: {
email: 'user@example.com',
password: 'password123'
}
}
};
// Story 3: Validation errors
export const WithErrors = {
args: {
errors: {
email: 'Email is required',
password: 'Password must be at least 8 characters'
}
}
};
// Story 4: Loading state
export const Loading = {
args: {
isLoading: true,
initialValues: {
email: 'user@example.com',
password: 'password123'
}
}
};
// Story 5: Submit error
export const SubmitError = {
args: {
submitError: 'Invalid email or password'
}
};
// Story 6: Interaction test with play function
export const InteractionTest = {
args: {},
play: async ({ canvasElement }) => {
// Canvas is the preview iframe
const canvas = within(canvasElement);
// Find form elements
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/password/i);
const submitButton = canvas.getByRole('button', { name: /log in/i });
// Simulate user interactions
await userEvent.type(emailInput, 'user@example.com');
await userEvent.type(passwordInput, 'password123');
// Verify form state
await expect(emailInput).toHaveValue('user@example.com');
await expect(passwordInput).toHaveValue('password123');
await expect(submitButton).toBeEnabled();
// Submit form
await userEvent.click(submitButton);
}
};
// Story 7: Accessibility test
export const AccessibilityTest = {
args: {
initialValues: {
email: 'user@example.com',
password: 'password123'
}
},
parameters: {
a11y: {
config: {
rules: [
{
id: 'label', // Ensure all inputs have labels
enabled: true
}
]
}
}
}
};
Advanced Example: Data Table with Complex States
// DataTable.stories.ts - TypeScript stories with complex data
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { DataTable } from './DataTable';
import { User } from './types';
// Generate mock data
const generateUsers = (count: number): User[] => {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: i % 3 === 0 ? 'admin' : 'user',
status: i % 2 === 0 ? 'active' : 'inactive'
}));
};
const meta: Meta<typeof DataTable> = {
title: 'Data/DataTable',
component: DataTable,
decorators: [
(Story) => (
<div style={{ padding: '20px' }}>
<Story />
</div>
)
],
argTypes: {
onRowClick: { action: 'row clicked' },
onSort: { action: 'sorted' },
onPageChange: { action: 'page changed' }
}
};
export default meta;
type Story = StoryObj<typeof DataTable>;
// Story 1: Empty state
export const Empty: Story = {
args: {
data: [],
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' }
]
}
};
// Story 2: Small dataset (5 rows)
export const SmallDataset: Story = {
args: {
data: generateUsers(5),
columns: [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email', sortable: true },
{ key: 'role', label: 'Role', sortable: true },
{ key: 'status', label: 'Status' }
]
}
};
// Story 3: Large dataset (1000 rows with pagination)
export const LargeDataset: Story = {
args: {
data: generateUsers(1000),
columns: [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email', sortable: true },
{ key: 'role', label: 'Role', sortable: true },
{ key: 'status', label: 'Status' }
],
pagination: {
page: 1,
pageSize: 20,
total: 1000
}
}
};
// Story 4: Loading state
export const Loading: Story = {
args: {
data: generateUsers(5),
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' }
],
isLoading: true
}
};
// Story 5: Error state
export const Error: Story = {
args: {
data: [],
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' }
],
error: 'Failed to load data. Please try again.'
}
};
// Story 6: Row selection
export const WithRowSelection: Story = {
args: {
data: generateUsers(10),
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' }
],
selectable: true,
selectedRows: [2, 5, 7]
}
};
// Story 7: Custom cell rendering
export const CustomCells: Story = {
args: {
data: generateUsers(5),
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{
key: 'role',
label: 'Role',
render: (value: string) => (
<span className={`badge badge-${value}`}>{value}</span>
)
},
{
key: 'status',
label: 'Status',
render: (value: string) => (
<span className={`status-${value}`}>
{value === 'active' ? 'on' : 'off'} {value}
</span>
)
}
]
}
};
// Story 8: Responsive mobile view
export const MobileView: Story = {
args: {
data: generateUsers(5),
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' }
]
},
parameters: {
viewport: {
defaultViewport: 'mobile1' // iPhone view
}
}
};
Common Mistakes
1. Not Creating Stories for Edge Cases
Mistake: Only creating “happy path” stories, missing error states, loading states, and empty data.
// ❌ BAD: Only one story showing ideal state
// UserList.stories.js
export default {
title: 'UserList',
component: UserList
};
export const Default = {
args: {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
}
};
// Missing: empty state, loading, error, 1000 users, long names
// ✅ GOOD: Cover all important states
export const Empty = {
args: {
users: []
}
};
export const Loading = {
args: {
users: [],
isLoading: true
}
};
export const Error = {
args: {
users: [],
error: 'Failed to load users'
}
};
export const LargeDataset = {
args: {
users: Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`
}))
}
};
export const LongNames = {
args: {
users: [
{ id: 1, name: 'Wolfeschlegelsteinhausenbergerdorff' }
]
}
};
export const WithPagination = {
args: {
users: generateUsers(100),
pagination: { page: 1, pageSize: 20 }
}
};
Why it matters: Edge cases reveal bugs that don’t appear in ideal conditions (overflow, layout breaks, poor error handling).
2. Hardcoding Component Data Instead of Using Args
Mistake: Rendering components with hardcoded props, making stories non-interactive.
// ❌ BAD: Hardcoded props, can't edit in Controls
export const PrimaryButton = () => (
<Button variant="primary" size="medium">
Click me
</Button>
);
// Controls addon shows nothing to edit
// ✅ GOOD: Use args for interactive controls
export const PrimaryButton = {
args: {
variant: 'primary',
size: 'medium',
children: 'Click me'
}
};
// Now you can edit variant/size/text in Controls panel
// Even better: Define argTypes for better controls
export default {
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
description: 'Button color variant'
},
size: {
control: 'radio',
options: ['small', 'medium', 'large']
},
disabled: {
control: 'boolean'
}
}
};
Why it matters: Args make stories interactive—designers can tweak values without code changes, and args are reusable across stories.
3. Forgetting Decorators for Context Providers
Mistake: Stories fail because components need context (theme, router, Redux store) but stories don’t provide it.
{% raw %}
// ❌ BAD: Component needs ThemeProvider but story doesn't wrap it
// ThemeButton.stories.js
export const Primary = {
args: { children: 'Click me' }
};
// ERROR: ThemeButton crashes because it calls useTheme() but no provider exists
// ✅ GOOD: Add decorator to provide context
import { ThemeProvider } from '../ThemeProvider';
export default {
component: ThemeButton,
decorators: [
(Story) => (
<ThemeProvider theme={{ primaryColor: '#0066cc' }}>
<Story />
</ThemeProvider>
)
]
};
export const Primary = {
args: { children: 'Click me' }
};
// ✅ EVEN BETTER: Global decorator in .storybook/preview.js
// .storybook/preview.js
import { ThemeProvider } from '../src/ThemeProvider';
export const decorators = [
(Story) => (
<ThemeProvider theme={{ primaryColor: '#0066cc' }}>
<Story />
</ThemeProvider>
)
];
// Now all stories have theme context automatically
// Multiple decorators example
export default {
component: UserProfile,
decorators: [
// Router context
(Story) => (
<BrowserRouter>
<Story />
</BrowserRouter>
),
// Redux store
(Story) => (
<Provider store={mockStore}>
<Story />
</Provider>
),
// Theme
(Story) => (
<ThemeProvider theme="light">
<Story />
</ThemeProvider>
)
]
};
Why it matters: Components often depend on context providers (React Router, Redux, theme). Without decorators, stories crash or render incorrectly.
Quick Quiz
References
- [1] https://storybook.js.org/docs/7/get-started/whats-a-story
- [2] https://storybook.js.org/docs/get-started/whats-a-story
- [3] https://storybook.js.org/blog/the-storybook-story/
- [4] https://www.youtube.com/watch?v=QbthZStwESI
- [5] https://storybook.js.org/docs/writing-stories
- [6] https://storybook.js.org/docs
- [7] https://storybook.js.org/docs/get-started/browse-stories