Stories: The Smallest Useful Contract for a UI

Storybook’s useful unit is not an abstract “component example”; it is a story with a recognizable shape. This chapter focuses on how Component Story Format, or CSF, turns a UI variation into structured metadata, inputs, rendering rules, and behavior. Understanding that shape lets you predict what Storybook can reuse, what it must be told explicitly, and why the same story can later serve controls, tests, documentation, and review.

A story has identity, inputs, and a way to render

A story file normally lives beside the component it describes. It contains a default export for component-level information and named exports for the individual variations. You can think of the default export as the shared description of the subject and each named export as one answer to the question, “What should this component look and behave like under these conditions?”

The default export can identify the component, give it a title in Storybook’s navigation, and attach settings that apply to its stories. The title is not merely decoration. It helps determine where the component appears in the hierarchy, while the named export supplies the story’s identity within that component. Storybook can also infer titles from the file’s location when the project uses the supported conventions. This is why a story has a stable address in the Storybook UI rather than being an anonymous render result.

The named export describes one state. In current CSF, it is generally a story object containing annotations such as args, parameters, decorators, or play. The object is concise when the default behavior is enough: Storybook knows the component from the meta and can render it with the story’s arguments. A story called Primary might provide a label and a primary flag; another story can provide the same component with a disabled or loading state. The important information is the difference between the states, not repeated instructions for how to mount the component.

CSF 3 made this data-oriented style more useful. Story objects can be spread and extended, so a related state can inherit the annotations of another state and override only what changes. Earlier function-based formats required more manual copying of properties when a story was reused. A spreadable object treats the state description as composable data. That is a small authoring choice with a larger consequence: when a component’s story data changes, composite stories that reuse it have a clearer path to staying aligned.

The format is also designed to remain portable beyond Storybook. It is based on JavaScript ES modules rather than a private serialization format. That does not mean every story runs unchanged in every environment; renderers, annotations, and setup still matter. It does mean the story’s basic representation can be interpreted by other tools instead of being locked inside one UI.

Args describe the variation

Storybook uses the word “args” for the named inputs that correspond to React props, Vue props, Angular inputs, and similar concepts in other runtimes. Args are the values that make one story different from another. They can contain a label, a boolean state, an array of options, a callback, or another value the component expects.

The distinction between a story and its args is useful. The story gives the variation an identity and a place in the component’s collection. Args describe the particular input values used for that variation. Storybook and its addons can then expose those values through Controls, allowing a person to change them interactively without editing the source file. That turns a story into more than a fixed illustration: it becomes a controlled starting point for exploring nearby states.

Args also support composition across a component hierarchy. Suppose a ButtonGroup is built from buttons. A group story can reuse the data from a button story instead of independently restating every button’s label and appearance. If the button’s story data changes, the group’s example has a better chance of reflecting the same change. The repository’s own stories demonstrate both inheritance and the boundary of this technique: targeted args can be grouped for a particular component, but a complex composition may still need a custom rendering strategy.

This data model has a practical test. If a story uses a custom render function, that function should pass the relevant args into the component it is meant to control. Otherwise the Controls panel may update the story’s data without changing the rendered component. The story remains valid JavaScript, but it has broken the connection that makes the state contract useful to the surrounding tools.

Render is the escape hatch for real compositions

The default render behavior covers the common case: take the component named in the meta and render it with the story’s args. A custom render function is needed when the variation is about a composition rather than a single direct component call. A list may need several child items. A button may need to appear inside an alert or a layout wrapper. A story may need to adapt its args before passing them to more than one element.

Custom rendering should add the minimum structure needed to express the state. It should not quietly replace the state inputs with unrelated constants. Passing args through the render function keeps the story editable and lets the rest of Storybook understand which values define the variation. This is a useful architectural boundary: the render function controls the shape of the output, while args remain the named inputs that other tools can inspect and change.

classDiagram class Story class Meta class Args class ArgTypes class Annotations class RenderFunction class PlayAndLifecycle class AddonsAndTools Story --> Meta : belongs to component Story --> Args : supplies state inputs Story --> ArgTypes : describes controls Story --> Annotations : inherits and overrides Annotations --> RenderFunction : configures rendering RenderFunction --> PlayAndLifecycle : produces state to exercise Annotations --> AddonsAndTools : exposes parameters and tags

The diagram is a conceptual map rather than a list of implementation classes. Meta gives a group of stories a subject and shared identity. Args and argTypes describe inputs and how tools can present them. Annotations add context and behavior. Rendering produces the UI state, and play or lifecycle work can exercise it. Parameters and tags let addons and integrations decide how to consume the result.

Annotations compose the conditions around the state

A component rarely renders correctly with props alone. It may need a theme, a router, a data provider, a locale, a mocked service, or a layout constraint. Storybook represents these conditions through annotations that can be applied at the project, component, or individual story level. The layers let a team define common context once and specialize it only where a particular state requires something different.

Decorators supply context and boundaries

A decorator wraps a story before it is rendered. A simple decorator might add padding so a component is not pressed against the preview boundary. A more meaningful one can provide a theme or a context provider. If a component expects a router, the decorator can put the story inside the appropriate routing context. If the component needs a mocked data client, the decorator can establish that client for the story.

The order and scope of decorators matter. A project-level decorator can establish a baseline for every story. A component-level decorator can add the conditions shared by one component’s variations. A story-level decorator can specialize a single case. The result is not just a stack of visual wrappers; it is a set of assumptions that determine what the component is actually being asked to render.

That makes decorators powerful and easy to misuse. A decorator can make a story representative of a real application context, but it can also hide a dependency if its setup is so broad that nobody knows why the component works. Good story design keeps the supplied context relevant to the state under examination. The story should make it possible to answer a focused question, not recreate the entire application by default.

Loaders prepare data before rendering

Loaders are asynchronous functions that prepare data for a story. Their results are made available through the story context, where a render function or decorator can use them. The repository’s loader example shows project, component, and story loaders contributing separate values before the component is rendered. This layered arrangement lets common fixtures live at a broader scope while a particular variation adds its own data.

The sequence is significant. The system applies the loader groups in project, component, and story order, and it waits for each group before moving to the next. That creates a predictable preparation boundary: the render starts after the story’s declared data setup has completed, unless the run has been aborted. A loader is therefore different from a random asynchronous side effect in a component. It declares data needed to establish the scenario.

Loaders do not turn an isolated story into a production integration test. A loader may fetch or construct a controlled fixture, while the real application may use a different service, cache, or error path. The value is that the component’s behavior under a known data condition can be reproduced and discussed. The remaining application wiring still needs its own coverage.

Parameters and tags tell tools what the story means

Parameters are static metadata. They do not primarily describe changing component inputs; they configure how Storybook or an addon should treat the story. A background setting can tell the preview which surfaces to offer. A layout parameter can affect how the story is framed. An accessibility parameter can set the behavior of that check. The same pattern makes it possible to configure an addon at project, component, or story scope.

Tags provide another kind of metadata. They can identify stories for development, testing, or automatic documentation, and a more specific story can add or remove tags inherited from a broader scope. This means a state can be present in the Storybook collection without necessarily participating in every automated workflow. For example, a deliberately demonstrated accessibility anti-pattern may remain useful as a story while being excluded from a blocking accessibility run.

These controls are part of the story’s contract with its consumers. A parameter or tag can change what a tool does with the story without changing the component’s visible inputs. That separation is useful, but it also creates a maintenance obligation. If a tag says a story should be tested while the story depends on unstable global state, the metadata does not make the test reliable. It only requests a workflow. The author still has to make the underlying state reproducible.

Play functions describe behavior after render

A play function is a small piece of code that runs after the story renders. It can query the story’s canvas, simulate a click or a keystroke, fill an input, call a spy, and assert on the result. In a form story, the initial args can represent an empty form, while play can represent what happens when a user submits it. The story then describes both the starting state and a meaningful transition from that state.

The separation between render and play is important. Render establishes the component and its initial conditions. Play exercises behavior that cannot be seen in a static state alone. A basic story with no play function can still act as a render or smoke check: it fails if the component cannot render successfully. A more complex state can add play when the question is whether a user action produces the correct outcome.

Storybook’s interaction guidance uses a queryable canvas, user-event methods, and an expect utility. These are not three unrelated conveniences. The canvas identifies the part of the page under test, user-event models an action, and the assertion evaluates the result. Awaiting interactions and assertions matters because Storybook records and debugs the sequence, especially in its Interactions panel.

For more advanced scenarios, the play function can use mount to control when the story is rendered. The current documentation places a specific constraint on this feature: the mount property must be destructured from the play context so Storybook can recognize the intended lifecycle, and the build must preserve the relevant modern syntax. This is a reminder that a story’s apparent simplicity rests on an execution contract between authoring code, the builder, and the preview runtime.

Static definition, live context

A story file defines a reusable state description, but a run needs live values. The runtime combines the project, component, and story annotations into a prepared story while keeping changing args, globals, hooks, and cleanup work in the context and related stores. This separation lets a control update an arg and request a new render without rewriting the story definition.

It also explains why state reset is part of correct story design. If a play function or loader changes module or component state, the next story should not inherit the previous run’s accidental residue. The interaction-testing documentation recommends project or component lifecycle hooks and cleanup functions for this purpose. A story is reproducible only when its setup and teardown are as deliberate as its initial args.

There are renderer-specific differences. Standard CSF uses a default export and named exports, while the documented Svelte CSF format uses a defineMeta function and Story components. Some standard CSF capabilities, including the portable APIs described elsewhere in Storybook’s documentation, do not apply identically to that alternative syntax. The general principle survives the difference: a story must expose a component state and enough annotations for the target runtime to render it.

The story contract is therefore small but not thin. It identifies a component, names a variation, supplies inputs, and declares the context and behavior needed to make that variation meaningful. Its usefulness comes from keeping those parts connected. The next question is what Storybook does with the definition once it is saved: how it discovers the file, gives the story an indexed identity, loads it, and turns the annotations into the isolated preview that a person can select.

References