A Storybook Story Is a Component Contract

October 10, 2025

A component can be perfectly reusable in source code and still be difficult to use. The API might expose ten props, but no one knows which combinations are valid. A loading state exists, but the only way to see it is to throttle a network request at the right moment. An error state looks acceptable in Figma and broken in the browser. The missing piece is often a shared, executable description of the component's behavior.

That is how I have come to think about a Storybook story: a small contract. It says, “Given this state and these inputs, this is what the component should render and how it should respond.” It can serve a designer reviewing a state, an engineer implementing a feature, and a test runner checking an interaction. Those audiences do not need identical information, but they benefit from the same example staying current.

Storybook has three overlapping uses: development, documentation, and testing. The important word is overlapping. A story written only as a screenshot can still be useful, but it leaves behavior unexplained. A story that exercises an important interaction can do more work without becoming a separate test application.

Choose states that answer real questions

Consider a PaymentStatus component. “Default” is a start, but the cases a team will actually ask about are more specific: pending, succeeded, declined, network error, and a very long merchant name. Each state should have a reason to exist. If two stories differ only in an internal prop that produces no meaningful change, one may be redundant.

// Illustrative Storybook story; adjust imports to your framework package.
import type { Meta, StoryObj } from '@storybook/react'
import { PaymentStatus } from './PaymentStatus'

const meta = {
  component: PaymentStatus,
  title: 'Checkout/PaymentStatus',
} satisfies Meta<typeof PaymentStatus>

export default meta
type Story = StoryObj<typeof meta>

export const Declined: Story = {
  args: {
    status: 'declined',
    message: 'Your card could not be charged.',
  },
}

The story is small because the component receives the state it needs. A reviewer can open it directly; a developer can see the prop contract; a visual test can compare the state across changes. The name “Declined” is more useful than “VariantThree” because it names the user-facing situation.

Separating components that render from components that load data can make their stories clearer. A presentational component fed by props is easy to put into a known state. A container component that fetches, transforms, and renders data needs a representative response and often a mock or fixture. Both can have stories, but they answer different questions. The first asks whether the UI communicates a state well. The second asks whether the surrounding behavior wires that state correctly.

This is a design boundary, not a command to split every file into two. If the component is already small and its data needs are trivial, adding a layer may create more ceremony than clarity. The useful test is whether you can create a meaningful state without recreating half of the application inside Storybook.

Move from a state to an interaction

A story can also express what should happen after the initial render. Storybook's play function runs after the story renders and can interact with the canvas. For a disclosure component, that might mean clicking “Details” and checking that the panel appears. For a form, it might mean entering an invalid email and checking the error message.

The value is not that every story must become a test. It is that the story already provides a stable setup for the scenario. An interaction check can reuse it instead of rebuilding the component's props and providers somewhere else. Storybook's testing documentation describes how stories can be run as component tests, including through its Vitest integration where supported.

This still leaves room for end-to-end tests. A checkout story can prove that a form reacts to a validation error; it cannot prove that a real payment provider accepted an order and that the order appears in an account. A story is a component boundary. Cross-service behavior needs a broader test.

Give stories the environment they actually need

A story that fails because it lacks the app's theme, router, or context provider is not a trustworthy contract. Global preview configuration and decorators can supply the same basic environment that components receive in the application. The shared environment should include what makes the component render honestly, without booting the entire product.

There is a balance. Too little setup gives false failures. Too much setup hides dependencies and slows stories down. If a simple button requires authentication, three API mocks, and a product-wide store to render, the component boundary may be carrying more responsibility than its name suggests.

I would make that visible during review. Which providers does the story need? Which data is fixture data? Does the interaction depend on a mock that behaves differently from production? A story that answers these questions openly is useful documentation for the component's architecture.

Know when the contract costs more than it saves

Storybook may be premature for a throwaway prototype. When the UI is changing hourly and the components are likely to be discarded, maintaining a story for each revision can slow exploration. Once a component becomes shared, complex, or business-critical, the value changes. A stable catalog of states helps teams review changes and onboard engineers without reproducing every edge case inside the product.

The same judgment applies to the number of stories. A library does not become a design system because every prop value has a story. A design system needs decisions about naming, accessibility, tokens, usage, and ownership. Storybook can make those decisions inspectable; it cannot make them on the team's behalf.

That is why I would start with a narrow set: the default state, an important alternate state, a failure state, and one meaningful interaction. Add stories when a bug, review, or support question shows that a state needs to be visible. Remove stories that no longer describe supported behavior.

The goal is a set of examples that developers trust. When a teammate asks how a component behaves, the story should answer before they have to read the implementation. When that behavior changes, the story should be one of the first places the change becomes obvious.

For more on this topic, watch Talks with Ido Evergreen: Storybook, Design Systems, and Frontend DX with Jeppe Reinhold.