How Storybook CSF Generates Story IDs with toId, toTestId, and storyNameFromExport
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. 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.
// 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.
// 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.
// 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 processes each export. It applies storyNameFromExport to derive the display name and toId to generate the unique identifier.
// 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 iterates through nested tests and assigns deterministic IDs using toTestId.
// 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:
// my-button.stories.ts
export default {
title: 'Components/Button',
};
export const primary = () => <Button primary label="Primary" />;
Internally, Storybook executes:
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:
export const primary = () => <Button primary label="Primary" />;
primary.test('clicks correctly', async ({ canvasElement }) => {
// Test implementation
});
The processing pipeline generates:
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 uses storyNameFromExport to detect unnecessary explicit name declarations:
// 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:
rules: {
'storybook/no-redundant-story-name': 'error',
}
Summary
storyNameFromExportconverts export keys likeprimaryButtoninto readable titles like"Primary Button"usingtoStartCaseStr.toIdgenerates URL-safe story identifiers in the format<kind>--<name>by sanitizing the component title and story name.toTestIdcreates 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, with consumption innormalizeStory.tsandprocessCSFFile.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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →