# How Storybook Story Composition Works with composeStories and composeStory

> Learn how Storybook's portable-stories API and composeStories enable you to run your stories outside the Storybook UI in Jest, Vitest, and Playwright.

- Repository: [Storybook/storybook](https://github.com/storybookjs/storybook)
- Tags: internals
- Published: 2026-02-27

---

**Storybook's portable-stories API transforms CSF (Component Story Format) modules into fully-composed story functions using `composeStories` for bulk operations and `composeStory` for individual stories, enabling execution outside the Storybook UI in test frameworks like Jest, Vitest, or Playwright.**

Storybook's **story composition** system allows developers to reuse stories written in CSF format across different environments without the Storybook UI. According to the `storybookjs/storybook` source code, this functionality is implemented in the core preview API and provides a bridge between static story definitions and executable test functions.

## The Core Composition Functions

The portable-stories API centers on two complementary utilities located in [`code/core/src/preview-api/modules/store/csf/portable-stories.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/preview-api/modules/store/csf/portable-stories.ts). These functions convert static CSF exports into callable `ComposedStoryFn` objects that include rendering logic, args, globals, and play helpers.

### composeStory

**`composeStory`** handles the composition of a single story annotation. Beginning at line 90 in [`portable-stories.ts`](https://github.com/storybookjs/storybook/blob/main/portable-stories.ts), this function accepts a story's annotations, the component's meta (default export), and optional project-level annotations. It returns a **`ComposedStoryFn`** that encapsulates the story's execution context, including methods like `load()`, `play()`, and `run()`.

### composeStories

**`composeStories`** provides bulk processing for entire CSF modules. Implemented starting at line 74 in the same file, this utility iterates over named exports, filters non-story members using `isExportStory`, and maps each valid story through a composition function (defaulting to `composeStory`). It returns an object keyed by story names with composed functions as values.

## How composeStory Works Internally

The composition process follows a deterministic normalization and preparation pipeline. In [`code/core/src/preview-api/modules/store/csf/portable-stories.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/preview-api/modules/store/csf/portable-stories.ts), `composeStory` executes these distinct phases:

1. **Input validation** – Throws immediately if story annotations are undefined (lines 97-100).

2. **Title resolution** – Falls back to `DEFAULT_STORY_TITLE` when component meta lacks a title (lines 104-106).

3. **Annotation normalization** – Processes three layers of configuration:
   - Component meta via `normalizeComponentAnnotations` (line 106-107)
   - Story name resolution using `exportsName` or `storyAnnotations.storyName` (lines 108-113)
   - Story annotation via `normalizeStory` (lines 115-119)
   - Project annotations via `normalizeProjectAnnotations`, merging global defaults and supplied configuration (lines 121-126)

4. **Story preparation** – Calls `prepareStory` to create a `PreparedStory` containing args, loaders, and the play function (lines 128-132).

5. **Globals construction** – Merges global-type defaults, initial globals, and story-specific globals (lines 134-140).

6. **Telemetry setup** – Instantiates a `ReporterAPI` for tracking (line 142).

7. **Context initialization** – Builds a `StoryContext` supplying hooks, globals, args, and canvas, marking it as portable via `__isPortableStory = true` (lines 144-165). This context powers the `play`, `run`, and callable story functions.

8. **Helper attachment** – Exposes `load` (runs loaders and beforeEach hooks), `play` (invokes the play function), and `run` (renders via `runStory`) through `Object.assign` (lines 25-66).

The returned object conforms to the `ComposedStoryFn` type defined in [`code/core/src/types/modules/composedStory.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/types/modules/composedStory.ts) (lines 40-55), providing a type-safe interface for test consumption.

## How composeStories Processes CSF Modules

**`composeStories`** automates batch composition while maintaining individual story integrity. The implementation extracts the default export as `metaExport` (component meta) and processes remaining exports through this workflow:

1. **Export extraction** – Captures the default export and individual story exports (lines 79-82).
2. **Meta resolution** – Uses `getCsfFactoryAnnotations` to obtain story and meta annotations, defaulting to the first encountered component meta if none is explicit (lines 84-88).
3. **Story filtering** – Removes non-story exports using `isExportStory` (lines 90-92).
4. **Composition mapping** – Applies the supplied `composeStoryFn` (defaulting to core `composeStory`) to each story and assigns results to the output object under their export names (lines 93-95).

This design supports the common pattern of `import * as stories from './Component.stories'` while ensuring only valid stories undergo composition.

## Practical Code Examples

### Composing a Single Story

Use `composeStory` when you need granular control over one specific story:

```typescript
import { composeStory } from '@storybook/react';
import * as meta from './Button.stories';

const Primary = composeStory(meta.CSF3Primary, meta.default);

// Execute in your test environment
await Primary.load();
await Primary.play({ canvasElement: document.body });

```

The `Primary` function now exposes `.play()`, `.run()`, and `.load()` methods, and can be called directly with custom args merged into the initial configuration.

### Composing All Stories in a Module

Use `composeStories` for bulk operations across entire story files:

```typescript
import { composeStories } from '@storybook/react';
import * as allStories from './Button.stories';

const { Primary, Secondary, WithLabel } = composeStories(allStories, {
  // Optional project-level annotations
  globals: { theme: 'dark' },
});

// Execute specific story interactions
await Secondary.load();
await WithLabel.play();

```

This approach automatically handles args, globals, and project configuration for every exported story, returning a plain object mapping story names to their composed functions.

### Custom Composition for Test Frameworks

Override the default composition behavior to inject framework-specific setup:

```typescript
import {
  composeStories,
  composeStory,
  type ComposeStoryFn,
} from '@storybook/react';
import * as module from './Button.stories';

const customCompose: ComposeStoryFn = (story, meta, project) => {
  const composed = composeStory(story, meta, project);
  const originalPlay = composed.play;
  
  // Inject viewport setup before play execution
  composed.play = async (ctx) => {
    await setViewport('iphone6');
    await originalPlay?.(ctx);
  };
  return composed;
};

const { Primary } = composeStories(module, {}, customCompose);
await Primary.play(); // Executes with forced viewport

```

By passing a custom `composeStoryFn` to `composeStories`, you maintain the core normalization logic while adding test-specific behaviors like viewport manipulation or mock setup.

## Summary

- **Story composition** converts static CSF exports into executable functions via `composeStory` and `composeStories` in [`code/core/src/preview-api/modules/store/csf/portable-stories.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/preview-api/modules/store/csf/portable-stories.ts).
- **`composeStory`** (line 90) processes individual stories through validation, normalization via `normalizeComponentAnnotations` and `normalizeStory`, preparation via `prepareStory`, and helper attachment (`load`, `play`, `run`).
- **`composeStories`** (line 74) filters exports using `isExportStory` and batches composition across entire modules, returning a map of `ComposedStoryFn` objects.
- The resulting functions include a portable context (`__isPortableStory = true`) that enables execution outside the Storybook UI with full support for args, globals, and play functions.
- Renderers like React, Vue, and Svelte re-export these core utilities from `code/renderers/**/portable-stories.ts` with framework-specific type definitions.

## Frequently Asked Questions

### What is the difference between composeStories and composeStory?

**`composeStory`** processes a single story annotation and its meta, returning one `ComposedStoryFn` suitable for isolated testing or dynamic selection. **`composeStories`** iterates over all named exports in a CSF module, filters non-stories using `isExportStory`, and composes each valid story, returning an object mapping story names to their composed functions. Use `composeStory` for individual story manipulation and `composeStories` for bulk import patterns common in test suites.

### How do I use composed stories in test frameworks like Jest or Vitest?

Import `composeStories` from your framework-specific package (e.g., `@storybook/react`), pass your stories module and optional project annotations, then destructure the specific stories you need. Each composed story provides `load()`, `play()`, and `run()` methods that execute within your test environment. The composed functions maintain full access to args, globals, and the canvas element, allowing you to test component interactions outside the Storybook UI.

### Can I customize the story composition process?

Yes. Both functions accept customization parameters. `composeStory` accepts optional project annotations to override globals and configuration. `composeStories` accepts a third parameter `composeStoryFn` where you can supply a custom implementation wrapping the core `composeStory`. This allows injection of test-specific behaviors like viewport setup, mock initialization, or telemetry reporting while preserving the normalization and preparation logic defined in [`code/core/src/preview-api/modules/store/csf/portable-stories.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/preview-api/modules/store/csf/portable-stories.ts).

### Where is the portable-stories API implemented in the Storybook source code?

The core implementation resides in [`code/core/src/preview-api/modules/store/csf/portable-stories.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/preview-api/modules/store/csf/portable-stories.ts), with `composeStories` starting at line 74 and `composeStory` at line 90. Type definitions for `ComposedStoryFn` and `ComposeStoryFn` are located in [`code/core/src/types/modules/composedStory.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/types/modules/composedStory.ts). Framework-specific renderers (React, Vue, Svelte) re-export these utilities from `code/renderers/[framework]/src/portable-stories.ts`, often providing type-safe overloads while delegating to the core composition logic.