# How Storybook CSF Generates Story IDs with toId, toTestId, and storyNameFromExport

> Discover how Storybook CSF uses storyNameFromExport, toId, and toTestId to generate stable story IDs, human-readable titles, and test IDs for your components. Learn the mechanics behind CSF story identification.

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

---

**Storybook's Component Story Format uses three deterministic utility functions—`storyNameFromExport`, `toId`, and `toTestId`—to convert JavaScript export names into URL-safe story identifiers, human-readable titles, and nested test IDs.**

Storybook's **Component Story Format (CSF)** v3 relies on a predictable identifier system to map exported story functions to stable URLs and test hierarchies. The `storybookjs/storybook` repository implements this logic in the core CSF module, where pure utility functions transform developer-friendly export names into sanitized, deterministic IDs used throughout the UI and testing infrastructure.

## Understanding the Core CSF Identifier Functions

The identifier generation pipeline centers on three utilities exported from [`code/core/src/csf/index.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/csf/index.ts). Each function serves a distinct purpose in the story normalization process.

### storyNameFromExport: Converting Code to Readable Names

The **`storyNameFromExport`** function transforms camelCase or PascalCase export names into human-readable story titles.

```typescript
// code/core/src/csf/index.ts
export const storyNameFromExport = (key: string) => toStartCaseStr(key);

```

When Storybook processes an export named `primaryButton`, this function returns `"Primary Button"`. This derived name becomes the default display title in the Storybook sidebar unless explicitly overridden by a `name` parameter.

### toId: Building URL-Safe Story Identifiers

The **`toId`** function constructs the canonical story ID by combining the component title (kind) with the story name, sanitizing both segments to remove illegal characters.

```typescript
// code/core/src/csf/index.ts
export const toId = (kind: string, name?: string) => 
  `${sanitizeSafe(kind, 'kind')}${name ? `--${sanitizeSafe(name, 'name')}` : ''}`;

```

This produces the familiar `<kind>--<name>` format, such as `components-button--primary`. The function relies on `sanitizeSafe` to strip punctuation and ensure URL compatibility.

### toTestId: Nesting Tests Under Stories

The **`toTestId`** function creates hierarchical identifiers for story tests, appending a test name to an existing story ID with a colon separator.

```typescript
// code/core/src/csf/index.ts
export const toTestId = (parentId: string, testName: string) => 
  `${parentId}:${sanitizeSafe(testName, 'test')}`;

```

This generates IDs like `button--primary:clicks-correctly`, enabling Storybook's test runner to associate individual tests with their parent stories.

## How Storybook Processes CSF Files to Generate IDs

The transformation from export keys to stable IDs occurs during the file parsing phase in Storybook's preview API.

### Normalizing Stories with normalizeStory

When Storybook loads a CSF file, the `normalizeStory` function in [`code/core/src/preview-api/modules/store/csf/normalizeStory.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/preview-api/modules/store/csf/normalizeStory.ts) processes each export. It applies `storyNameFromExport` to derive the display name and `toId` to generate the unique identifier.

```typescript
// code/core/src/preview-api/modules/store/csf/normalizeStory.ts
const exportName = storyNameFromExport(key);          // "PrimaryButton" → "Primary Button"
const id = parameters.__id || toId(meta.id, exportName); // "components-button--primary"

```

The `meta.id` represents the component title, while `exportName` provides the story-specific suffix. If the story definition includes an explicit `__id` parameter, Storybook uses that instead of generating a new one.

### Processing Test Children with processCSFFile

For stories that include `.test` children, the `processCSFFile` function in [`code/core/src/preview-api/modules/store/csf/processCSFFile.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/preview-api/modules/store/csf/processCSFFile.ts) iterates through nested tests and assigns deterministic IDs using `toTestId`.

```typescript
// code/core/src/preview-api/modules/store/csf/processCSFFile.ts
const childId = toTestId(storyMeta.id, name); // name = child.test.name
child.input.parameters.__id = childId;        // Force the child to keep this ID
csfFile.stories[childId] = normalizeStory(name, child.input as any, meta);

```

This registration process treats each test as a distinct story entry while maintaining the hierarchical relationship through the ID naming convention.

## Practical Code Examples

The following examples demonstrate how these utilities work in practice across different Storybook workflows.

### Generating a Story ID from an Export

Consider a basic button story file:

```typescript
// my-button.stories.ts
export default {
  title: 'Components/Button',
};

export const primary = () => <Button primary label="Primary" />;

```

Internally, Storybook executes:

```typescript
const key = 'primary';
const exportName = storyNameFromExport(key);           // "Primary"
const storyId = toId('Components/Button', exportName); // "components-button--primary"

```

### Creating Test IDs for Story Interactions

When adding tests to a story:

```typescript
export const primary = () => <Button primary label="Primary" />;
primary.test('clicks correctly', async ({ canvasElement }) => {
  // Test implementation
});

```

The processing pipeline generates:

```typescript
const parentId = 'components-button--primary';
const testName = 'clicks correctly';
const testId = toTestId(parentId, testName); // "components-button--primary:clicks-correctly"

```

### Validating Story Names with ESLint

The `no-redundant-story-name` lint rule in [`code/lib/eslint-plugin/src/rules/no-redundant-story-name.ts`](https://github.com/storybookjs/storybook/blob/main/code/lib/eslint-plugin/src/rules/no-redundant-story-name.ts) uses `storyNameFromExport` to detect unnecessary explicit name declarations:

```typescript
// Rule implementation
const resolvedStoryName = storyNameFromExport(name);
if (story.name && story.name !== resolvedStoryName) {
  context.report({ 
    node, 
    message: 'Story name should match export name' 
  });
}

```

Configure this rule in your [`.eslintrc.js`](https://github.com/storybookjs/storybook/blob/main/.eslintrc.js):

```javascript
rules: {
  'storybook/no-redundant-story-name': 'error',
}

```

## Summary

- **`storyNameFromExport`** converts export keys like `primaryButton` into readable titles like `"Primary Button"` using `toStartCaseStr`.
- **`toId`** generates URL-safe story identifiers in the format `<kind>--<name>` by sanitizing the component title and story name.
- **`toTestId`** creates hierarchical test identifiers using the pattern `<story-id>:<test-name>` for nested test definitions.
- **Source locations**: These utilities live in [`code/core/src/csf/index.ts`](https://github.com/storybookjs/storybook/blob/main/code/core/src/csf/index.ts), with consumption in [`normalizeStory.ts`](https://github.com/storybookjs/storybook/blob/main/normalizeStory.ts) and [`processCSFFile.ts`](https://github.com/storybookjs/storybook/blob/main/processCSFFile.ts).
- **Deterministic output**: All three functions rely on shared sanitization routines to ensure consistent, URL-friendly IDs across the Storybook ecosystem.

## Frequently Asked Questions

### How does Storybook handle special characters in story names?

Storybook passes all strings through `sanitizeSafe` before incorporating them into IDs. This function removes punctuation, spaces, and illegal URL characters, converting them to safe equivalents. For example, a story named `"Primary (Active)"` becomes `primary-active` in the final ID.

### Can I override the auto-generated story ID?

Yes. While `normalizeStory` automatically calls `toId` when no ID exists, you can specify a custom `__id` in the story's parameters. When `parameters.__id` is present, Storybook uses that value directly instead of generating one from the export name.

### What is the difference between a story ID and a test ID?

A **story ID** uses the double-dash separator (`--`) to combine the component kind and story name, such as `button--primary`. A **test ID** uses a colon separator (`:`) to nest a test under its parent story, such as `button--primary:click-handler`. The `toTestId` function specifically handles this namespacing for test framework integration.

### Where does the human-readable story title come from if I don't specify a name property?

Storybook derives the display title automatically using `storyNameFromExport`. This function converts the JavaScript export name from camelCase or PascalCase into start case. An export named `secondaryButtonWithIcon` renders as `"Secondary Button With Icon"` in the Storybook sidebar unless you provide an explicit `name` parameter.