Component Story Format: Making React Component Docs Executable
Component Story Format turns isolated React components into interactive documentation with built-in controls and executable tests. Here is how the pieces fit together.
09 Nov 2025, 10:42 UTC

Most component libraries start with good intentions. Someone writes a README, adds a few examples, and links it from the design system. Six months later, the README describes a prop that no longer exists, the examples render a component with the wrong default state, and nobody trusts the docs enough to use them without reading the source. The drift between documentation and implementation is expensive because it forces every consumer to verify the truth manually.
Storybook's Component Story Format (CSF) offers a different contract: put the component's states, its interactive controls, and its assertions in the same file. If the component changes, the story breaks or the docs update themselves. The thesis here is simple—treat the story file as an executable specification, not a gallery of screenshots.
What a CSF file actually declares
CSF is an ES module convention. A .stories.tsx file exports a default object that describes the component, and named exports that describe individual states. In CSF v3—the version current Storybook 7+ expects—the default export carries title, component, and optional metadata like tags or argTypes. Each named export is a story function or an object with a render property.
The render prop matters because it decouples the story from a hard-coded JSX tree. You receive the current args and return the element you want to display. That indirection is what makes the controls panel useful later.
One version note: CSF v1 used a different export structure and is deprecated. If you are upgrading from Storybook 6, check the migration guide before assuming your existing stories will load unchanged.
Args are the bridge to interactive docs
An args object on a story maps prop names to values. Storybook reads that object and generates form controls in the addon panel. Change a value in the UI, and the component re-renders with the new prop. This turns a static example into a sandbox where a designer or a backend engineer can explore the component's behavior without touching code.
TypeScript improves this experience, but it is not automatic. If your component's props are typed, Storybook can often infer control types. When inference fails—or when you need to restrict a control to a specific set of options—you define argTypes explicitly in the default export. Treat that as a maintenance cost: the story file now knows about your component's prop schema in two places.
Play functions make the story a test
A play function runs after the story renders. It receives a context object with utilities for querying the DOM and asserting on it. Because Storybook ships with Testing Library integration, you can write interactions and assertions using the same queries you would use in a unit test.
This is where CSF stops being a documentation tool and becomes a test runner. The same file that shows a product manager what a disabled button looks like can also assert that the button is actually disabled. The constraint is version and dependency: play functions require Storybook 7.0 or higher, and the Testing Library package must be present in your project.
A worked example
Imagine a Button component that accepts label, variant, and disabled. Here is a CSF v3 story that documents the default state, exposes controls, and includes a basic assertion.
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof Button>;
export const Default: Story = {
args: {
label: 'Submit',
variant: 'primary',
disabled: false,
},
};
export const Disabled: Story = {
args: {
label: 'Submit',
variant: 'primary',
disabled: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button', { name: /submit/i });
if (!(button as HTMLButtonElement).disabled) {
throw new Error('Button should be disabled');
}
},
};
To verify this locally, run npx storybook init in the root of an existing React project if Storybook is not installed yet. Then check package.json for @storybook/react at version 7.x or higher. Start the dev server with npm run storybook (or the equivalent script). In the browser, open the Components/Button story. You should see the controls panel populated with inputs for label, variant, and disabled. Toggling disabled should update the rendered button immediately.
| Check | Where to look | Expected result |
|---|---|---|
| Storybook version | package.json |
@storybook/react at 7.x or higher |
| Testing Library | package.json |
@storybook/testing-library present |
| Controls render | Browser, Controls panel | Inputs for each arg appear |
For the play function, open the Interactions panel in the Storybook UI. If the component behaves as expected, the panel shows the assertion passing. If Testing Library is missing, Storybook will log an import error when the story loads.
Trade-offs worth planning for
CSF concentrates power in one file, which also concentrates risk. A story file with heavy play logic becomes a test suite that happens to render UI; if your team separates test authors from documentation authors, that split becomes awkward. The argTypes duplication mentioned earlier is another maintenance surface: refactor a prop name and you must update both the component and the story metadata.
Auto-docs, which generates MDX pages from your stories, is convenient but only as good as your story names and descriptions. A story named Default with no parameters produces a docs page that says very little. Treat the story names as API surface.
Start with one component
You do not need to migrate an entire library in one sprint. Pick a component that changes often or that new engineers frequently misunderstand. Write a CSF v3 story with explicit args, add one play assertion for the most common failure mode, and wire it into your CI pipeline. If the controls panel helps a teammate answer a question without opening the source, the format is doing its job.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.