When writing UI, prefer breaking larger components up into smaller parts.
ALWAYS write a Storybook story for any component written. If editing a component, ensure appropriate changes have been made to stories for that component.
Goal: Cover every distinct piece of business logic and state the component can reach (happy paths, error/edge states, loading, permissions/roles, empty states, variations from props/context). Avoid redundant stories that show the same logic.
Interactivity: If the component is interactive, add Interaction tests using play functions that drive the UI with storybook/test utilities (e.g., fn, userEvent, expect). Simulate key user flows: clicking buttons/links, typing, focus/blur, keyboard nav, form submit, async responses, toggle/selection changes, pagination/filters, etc. When passing fn functions as args for callback functions, make sure to add a play function which interacts with the component and assert whether the callback function was actually called.
Data/setup: Provide realistic props, state, and mocked data. Include meaningful labels/text to make behaviors observable. Stub network/services with deterministic fixtures; keep stories reliable.
Assertions: In play functions, assert the visible outcome of the interaction (text, aria state, enabled/disabled, class/state changes, emitted events). Prefer role/label-based queries.
Variants to consider (pick only those that change behavior): default vs. alternate themes; loading vs. loaded vs. empty vs. error; validated vs. invalid input; permissions/roles/capabilities; feature flags; size/density/layout variants that alter logic.
Accessibility: Use semantic roles/labels; ensure focusable/keyboard interactions are test-covered where relevant.
Naming/structure: Use clear story names that describe the scenario (“Error state after failed submit”). Group related variants logically; don’t duplicate.
Imports/format: Import Meta/StoryObj from the framework package; import test helpers from storybook/test (not @storybook/test). Keep stories minimal—only what's needed to demonstrate behavior.
Update story imports to use the framework package:
- import { Meta, StoryObj } from '{{RENDERER}}';
+ import { Meta, StoryObj } from '{{FRAMEWORK}}';Update test imports to use storybook/test instead of @storybook/test
- import { fn } from '@storybook/test';
+ import { fn } from 'storybook/test';The globals annotation has be renamed to initialGlobals:
// .storybook/preview.js
export default {
- globals: { theme: 'light' }
+ initialGlobals: { theme: 'light' }
};Instead of parameters.docs.autodocs in main.js, use tags:
// .storybook/preview.js or in individual stories
export default {
tags: ['autodocs'], // generates autodocs for all stories
};To mock imports in Storybook, use Storybook's mocking features. ALWAYS mock external dependencies to ensure stories render consistently.
- Register in the mock in Storybook's preview file:
To mock dependendencies, you MUST register a module mock in
.storybook/preview.ts(or equivalent):
import { sb } from 'storybook/test';
// Prefer spy mocks (keeps functions, but allows to override them and spy on them)
sb.mock(import('some-library'), { spy: true });Important: Use file extensions when referring to relative files!
sb.mock(import('./relative/module.ts'), { spy: true });- Specify mock values in stories:
You can override the behaviour of the mocks per-story using
beforeEachand themocked()type function:
import { expect, mocked, fn } from 'storybook/test';
import { library } from 'some-library';
const meta = {
component: AuthButton,
beforeEach: async () => {
mocked(library).mockResolvedValue({ user: 'data' });
},
};
export const LoggedIn: Story = {
play: async ({ canvas }) => {
await expect(library).toHaveBeenCalled();
},
};Before doing this ensure you have mocked the import in the preview file.
- The play function has a
canvasparameter that can be used directly with testing-library-like query methods. - It also has a
canvasElementwhich is the actual DOM element. - The
within-function imported fromstorybook/testtransforms a DOM element to an object with query methods, similar tocanvas.
DO NOT use within(canvas) - it is redundant because canvas already has the query methods, canvas is not a DOM element.
// ✅ Correct: Use canvas directly
play: async ({ canvas }) => {
await canvas.getByLabelText('Submit').click();
};
// ⚠️ Also acceptable: Use `canvasElement` with `within`
import { within } from 'storybook/test';
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.getByLabelText('Submit').click();
};
// ❌ Wrong: Do NOT use within(canvas)
play: async ({ canvas }) => {
const screen = within(canvas); // Error!
};- Node.js 20+, TypeScript 4.9+
- React Native uses
.rnstorybookdirectory
- ALWAYS provide story links after any changes to stories files, including changes to existing stories.
- {{STORY_LINKING_WORKFLOW}}
- When sharing links, choose the most relevant subset for the user and avoid long lists (generally no more than 5 links).
- {{CHANGED_STORY_FALLBACK_LINK_GUIDANCE}}
- After changing any UI components, ALWAYS search for related stories that might cover the changes you've made. If you find any, provide the story links to the user. THIS IS VERY IMPORTANT, as it allows the user to visually inspect the changes you've made. Even later in a session when changing UI components or stories that have already been linked to previously, YOU MUST PROVIDE THE LINKS AGAIN.