with one click
storybook
// Storybook best practices for Wonder Blocks component stories. Use when creating or editing `.stories.tsx` / `.stories.ts` files.
// Storybook best practices for Wonder Blocks component stories. Use when creating or editing `.stories.tsx` / `.stories.ts` files.
Jest + React Testing Library best practices for Wonder Blocks unit tests. Use when creating or editing `.test.ts` / `.test.tsx` files.
Implements user interfaces using the Wonder Blocks (WB) design system — Khan Academy's React component library. Use this skill whenever the user asks you to build, modify, or review UI components; wants to use or map WB tokens for colors/spacing/typography (including translating Figma designs to WB components and tokens); or asks how to do something "the Wonder Blocks way". If the user is building any kind of form, layout, modal, button, dropdown, or typography treatment in a WB-enabled codebase, this skill applies — even if they don't explicitly say "Wonder Blocks". Do NOT trigger for debugging TypeScript errors, writing tests, or fixing CI/lint issues in WB packages.
| name | storybook |
| description | Storybook best practices for Wonder Blocks component stories. Use when creating or editing `.stories.tsx` / `.stories.ts` files. |
This guide covers conventions and best practices for creating Storybook stories (.stories.tsx or .stories.ts) in the Wonder Blocks design system.
✅ Define story types consistently (avoid any):
type StoryComponentType = StoryObj<typeof Component>;
export const Default: StoryComponentType = {
args: {
// props here
},
};
✅ Include all relevant meta properties:
export default {
title: "Packages / ComponentName / SubComponent",
component: ComponentName,
subcomponents: {SubComponent1, SubComponent2}, // If applicable
parameters: {
componentSubtitle: (
<ComponentInfo
name={packageConfig.name}
version={packageConfig.version}
/>
),
chromatic: {
disableSnapshot: false, // or true with reason
},
},
argTypes: ComponentArgTypes,
args: {
// Default args for all stories
},
decorators: [
// Optional decorators
],
} as Meta<typeof ComponentName>;
Key properties:
title: Hierarchical path in Storybook sidebarcomponent: The main component being documentedsubcomponents: Related components shown in docsparameters: Meta-level configuration (Chromatic, a11y, etc.)argTypes: Control definitions (usually imported from separate file)args: Default values applied to all storiesdecorators: Layout wrappers applied to all stories✅ Follow the hierarchy (avoid flat titles like "Button Stories"):
title: "Packages / Button / Button"
title: "Packages / Dropdown / SingleSelect"
title: "Packages / Button / Testing / Snapshots / ActivityButton"
✅ Follow these best practices when writing stories:
✅ Export stories with descriptive JSDoc comments:
/**
* This is the default state of the button showing standard usage.
* It demonstrates the basic props and expected behavior.
*/
export const Default: StoryComponentType = {
args: {
children: "Click me",
onClick: () => {},
},
};
✅ Use render functions for stateful stories:
export const WithState: StoryComponentType = {
render: function Render(args) {
const [value, setValue] = React.useState(args.value || "");
return (
<Component
{...args}
value={value}
onChange={setValue}
/>
);
},
args: {
// initial args
},
};
⚠️ Important: Use function declarations, not arrow functions:
// ✅ Good - Named function for better debugging
render: function Render(args) {
// ...
}
// ❌ Avoid - Arrow functions don't have clear names
render: (args) => {
// ...
}
✅ Show different variants in a single story:
/**
* Buttons have three kinds: `primary` (default), `secondary`, and `tertiary`.
*/
export const Kinds: StoryComponentType = {
render: () => (
<View style={{gap: spacing.medium_16}}>
<Button onClick={() => {}}>Primary</Button>
<Button kind="secondary" onClick={() => {}}>Secondary</Button>
<Button kind="tertiary" onClick={() => {}}>Tertiary</Button>
</View>
),
};
✅ Include comprehensive JSDoc comments:
/**
* This example demonstrates how SingleSelect behaves with an initial value.
* The screen reader will not announce the initial value on mount, but will
* announce when the value changes through user interaction.
*/
export const WithInitialValue: StoryComponentType = {
// story configuration
};
"union"), the type can be overridden in an argTypes.ts file.Visual style, Events, Accessibility. Use the table.category property in argTypes to group props.When to use: Override auto-generated prop types when they're not helpful or need customization.
✅ Create an argTypes file for your component:
// __docs__/wonder-blocks-button/button.argtypes.ts
import type {ArgTypes} from "@storybook/react-vite";
export default {
// Override type display for union types
kind: {
control: {type: "select"},
options: ["primary", "secondary", "tertiary"],
table: {
category: "Visual style",
type: {summary: `"primary" | "secondary" | "tertiary"`},
defaultValue: {summary: `"primary"`},
},
},
// Group related props
size: {
control: {type: "select"},
table: {
category: "Layout",
type: {summary: `"medium" | "small" | "large"`},
},
},
// Improve descriptions for complex props
style: {
table: {
category: "Layout",
type: {summary: "StyleType"},
},
},
} satisfies ArgTypes;
✅ Use argTypes in your story's meta:
import ComponentArgTypes from "./component.argtypes";
export default {
title: "Packages / Component",
component: Component,
argTypes: ComponentArgTypes,
} as Meta<typeof Component>;
Common argTypes configurations:
| Property | Purpose |
|---|---|
control.type | Control widget ("select", "boolean", "text", etc.) |
options | Available options for select controls |
table.category | Group props in the docs table |
table.type.summary | Override the displayed type |
table.defaultValue.summary | Show default value in docs |
mapping | Map control values to actual prop values |
✅ Disable snapshots with clear reasoning:
export const Interactive: StoryComponentType = {
render: () => {/* ... */},
parameters: {
chromatic: {
// Disabling because this is for manual testing purposes
disableSnapshot: true,
},
},
};
✅ Configure snapshot timing when needed:
export const ControlledOpened: StoryComponentType = {
render: (args) => <Component {...args} />,
parameters: {
// Added to ensure that the dropdown menu is rendered using PopperJS.
chromatic: {delay: 500},
},
};
✅ Enable snapshots for important visual states:
export const WithIcon: StoryComponentType = {
render: () => <IconExample />,
parameters: {
chromatic: {
modes: themeModes, // Test in multiple themes
},
},
};
Theme modes allow Chromatic to capture snapshots of components in multiple themes (e.g., default and Khanmigo/ThunderBlocks themes).
✅ Import and use themeModes for visual regression testing:
import {themeModes} from "../../.storybook/modes";
export default {
title: "Packages / Component / Testing / Snapshots",
parameters: {
chromatic: {
modes: themeModes, // Captures snapshots in all themes
},
},
tags: ["!autodocs"],
} as Meta<typeof Component>;
⚠️ Use theme modes sparingly - Each mode multiplies the number of Chromatic snapshots. Only add theme modes to snapshot stories that specifically test theming or visual appearance.
⚠️ Important: StateSheet and Scenarios stories are specifically designed for Chromatic visual regression testing. These stories systematically capture different states and edge cases to ensure visual consistency across code changes.
✅ Create dedicated snapshot stories:
/**
* The following stories are used to generate the pseudo states for the
* ActivityButton component. This is only used for visual testing in Chromatic.
*/
export default {
title: "Packages / Button / Testing / Snapshots / ActivityButton",
tags: ["!autodocs"], // Exclude from auto-generated docs
parameters: {
chromatic: {
modes: themeModes,
},
},
} as Meta;
✅ Use StateSheet for pseudo-state testing (Chromatic visual regression):
const kinds = [
{name: "Primary", props: {kind: "primary"}},
{name: "Secondary", props: {kind: "secondary"}},
{name: "Tertiary", props: {kind: "tertiary"}},
];
const actionTypes = [
{name: "Progressive", props: {actionType: "progressive"}},
{name: "Neutral", props: {actionType: "neutral"}},
{name: "Disabled", props: {disabled: true}},
];
export const StateSheetStory: Story = {
name: "StateSheet",
render: (args) => (
<StateSheet rows={kinds} columns={actionTypes} title="Kind / Action Type">
{({props, className}) => (
<Component {...args} {...props} className={className} />
)}
</StateSheet>
),
parameters: {
pseudo: defaultPseudoStates, // Includes focus, hover, active states
},
};
⚠️ StateSheet stories should cover: focus (:focus-visible), hover, active, disabled states, and relevant prop combinations (kind, actionType, size, etc.)
✅ Test edge cases with ScenariosLayout (Chromatic visual regression):
export const Scenarios: Story = {
render() {
const scenarios = [
{name: "Long label", props: {children: <Component>{longText}</Component>}},
{name: "RTL", decorator: <div dir="rtl" />, props: {children: <Component>یہ اردو میں لکھا ہے۔</Component>}},
];
return (
<ScenariosLayout scenarios={scenarios}>
{(props) => props.children}
</ScenariosLayout>
);
},
};
⚠️ Scenario stories should include: RTL layouts (use decorator: <div dir="rtl" />), custom style overrides, and edge cases (long text, long text with no word break, overflow, truncation, empty states).
✅ Create playtesting stories for testing of specific behaviors:
export default {
title: "Packages / Component / Testing / Component - Playtesting",
parameters: {
chromatic: {disableSnapshot: true}, // For testing purposes only, snapshots are not needed
},
} as Meta<typeof Component>;
/**
* Describe the scenario and what to test manually.
* Example: "When selecting a tab with Space/Enter, the page should not scroll."
*/
export const ScrollBehavior: Story = {
render: (args) => (
// Set up the scenario for manual testing
),
};
Good candidates for playtesting stories:
⚠️ Playtesting stories should: disable Chromatic snapshots, include clear JSDoc comments explaining what to test, and use the Testing / Component - Playtesting title pattern.
✅ Use clear, descriptive names:
// ✅ Good - Clear and descriptive
export const Default: StoryComponentType = {/* ... */};
export const WithIcon: StoryComponentType = {/* ... */};
export const Disabled: StoryComponentType = {/* ... */};
export const LongOptionLabels: StoryComponentType = {/* ... */};
export const ErrorFromValidation: StoryComponentType = {/* ... */};
✅ Override story names when needed:
export const WithRouter: StoryComponentType = {
name: "Navigation with React Router", // Overrides story name in UI
render: () => {/* ... */},
};
✅ Do not duplicate the existing story name
Named exports should not use the name annotation if it is redundant to the name that would be generated by the export name.
✅ Use storybook actions for event logging:
import {action} from "storybook/actions";
export const Default: StoryComponentType = {
args: {
onClick: action("clicked"),
onChange: action("changed"),
},
};
For stateful stories, combine actions with state updates: action("onChange")(newValue); setValue(newValue);
⚠️ Use Storybook interaction tests for behaviors that rely on real browser APIs not available in jsdom (scroll, layout/geometry, clipboard, complex focus management).
⚠️ Don't test styling in interaction tests - visual appearance is covered by Chromatic snapshot tests.
✅ Use the play function:
import {expect, within} from "storybook/test";
export const BrowserBehaviorTest: StoryComponentType = {
render: (args) => (
// Set up the DOM structure needed to test the behavior
),
play: async ({canvasElement}) => {
const canvas = within(canvasElement);
// Interact with the component (if needed)
// Assert things based on browser-specific behavior
// (e.g., ResizeObserver, getBoundingClientRect, scrollIntoView)
},
parameters: {
/** These stories are used for testing purposes only so we disable snapshots */
chromatic: {disableSnapshot: true},
},
};
⚠️ It's okay to have both: Unit tests with proper mocking work for browser behavior, but interaction tests provide additional confidence in a real browser. See .agents/skills/unit-tests/SKILL.md for mocking guidance.
title: "Packages / Button / Testing / Snapshots" with tags: ["!autodocs"] for snapshot storieschromatic: {disableSnapshot: true} for interaction tests and playtesting storiesReact.FC - Use (props: Props) => insteadplay functions for browser-specific behavior that jsdom can't handle