.storybook/preview.js
src/some-component.stories.tsx - Meta
src/some-component.stories.tsx - Story
Every story
Groups of stories
One story
import { Meta, StoryObj } from "@storybook/react"
import { MyComponent } from "./MyComponent"
// Create Meta
const meta: Meta<typeof MyComponent> = {
title: 'Path/To/MyComponent',
component: MyComponent,
};
export default meta;
// Create a story type
type Story = StoryObj<typeof MyComponent>;
// Write stories
export const SomeName: Story = {
name: "Some Name",
args: {
someProp: "Some value",
},
};
1. Create Meta
2. Create Story type
3. Write stories
// Don't import Story
import { Meta, Story } from "@storybook/react"
import { MyComponent } from "./MyComponent"
// `as` loses the type check
export default {
title: 'Path/To/MyComponent',
component: MyComponent,
} as Meta;
// Use types and render methods instead of templates
const Template: Story<MyComponentProps> = (...args) => (
<MyComponent {...args} />
)
// No need to bind anymore
const SomeName = Template.bind({})
SomeName.name = "Some Name"
SomeName.args = {
someProp: "Some value",
}
// Story exports should go inline
export { Default }
import { Meta, StoryObj } from "@storybook/react"
import { MyComponent } from "./MyComponent"
// Create Meta
const meta: Meta<typeof MyComponent> = {
title: 'Path/To/MyComponent',
component: MyComponent,
};
export default meta;
// Create a story type
type Story = StoryObj<typeof MyComponent>;
// Write stories
export const SomeName: Story = {
name: "Some Name",
args: {
someProp: "Some value",
},
};
Old Way
New Way
const meta: Meta<typeof MyComponent> = {
title: 'Components/ContactCardCheckout/States',
component: MyComponent,
};
Component Name
export const Default: Story = {
args: {
heading: "Some Heading",
},
}
export const Valid: Story = {
args: {
...Default.args,
isValid: true,
},
}
export const Invalid: Story = {
args: {
...Default.args,
isValid: false,
},
}
Just spread!
export const SomeName: Story = {
name: "Some Name",
render: (args) => {
const [someState, setSomeState] = useState("")
return (
<MyComponent
{...args}
someState={someState}
setSomeState={setSomeState}
/>
)
},
args: {
someProp: "Some value",
},
};
Any JavaScripty stuff has to happen here!
export const SomeInteraction = {
...SomeState
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await click(canvas.getByRole("button", { name: /Add/i }));
expect(args.someCallback).toHaveBeenCalledWith("Hello, world!")
},
}
Arrange
Assert
Act
// Use these instead of the Jest/RTL/User-Event versions
import { expect, within, fn, userEvent, waitFor } from "@storybook/test";
// These are interaction helpers, use these instead of DOM events
const { hover, click, dblClick, type, keyboard } = userEvent;
import { clickAdd } from "../steps";
export const SomeStory = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
// Inline
await step("Click add", () => {
await click(await canvas.findByRole("button", { name: /Add/i }));
});
// Reuse a step
await step("Click add", clickAdd(canvas));
},
}
Steps are a way to make interaction tests more semantic and composeable
const fixtures: Record<string, Contact> = {
primaryHomeOwner: {
contactId: "1",
customerFirstName: "Primary",
customerLastName: "Homeowner",
customerPrimaryPhone: "(123) 456-7890",
customerEmail: "primary.homeowner@email.com",
primary: true,
roles: ["Homeowner"],
derivedRoles: ["Homeowner"],
},
// Others
};
export const createFixture = (key: keyof typeof fixtures) =>
structuredClone(fixtures[key]);
Fixtures make data more semantic and reusable