docs/api/csf/csf-next.mdx
<If notRenderer={['react', 'vue', 'angular', 'web-components']}>
<Callout variant="info"> CSF Next is currently only supported in [React](?renderer=react), [Vue](?renderer=vue), [Angular](?renderer=angular), and [Web Components](?renderer=web-components) projects. </Callout> </If><If renderer={['react', 'vue', 'angular', 'web-components']}>
<Callout variant="warning" icon="๐งช"> This is a [**preview**](../../releases/features.mdx#preview) feature and (though unlikely) the API may change in future releases. We [welcome feedback](https://github.com/storybookjs/storybook/discussions/30112) and contributions to help improve this feature. </Callout>CSF Next is the next evolution of Storybook's Component Story Format (CSF). This new API uses a pattern called factory functions to provide full type safety to your Storybook stories, making it easier to configure addons correctly and unlocking the full potential of Storybook's features.
This reference provides an overview of the API and a migration guide to upgrade from prior CSF versions.
The CSF Next API is composed of functions to help you write stories. Note how three of the functions operate as factories, each producing the next function in the chain (definePreview โ preview.meta โ meta.story), providing full type safety at each step.
defineMainWith CSF Next, your main Storybook config is specified by the defineMain function. This function is type-safe and will automatically infer types for your project.
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { defineMain } from '@storybook/your-framework/node';
export default defineMain({
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-a11y'],
});
definePreviewSimilarly, the definePreview function specifies your project's story configuration. This function is also type-safe and will infer types throughout your project.
Importantly, by specifying addons here, their types will be available throughout your project, enabling autocompletion and type checking.
You will import the result of this function, preview, in your story files to define the component meta.
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { definePreview } from '@storybook/your-framework';
import addonA11y from '@storybook/addon-a11y';
export default definePreview({
// ๐ Add your addons here
addons: [addonA11y()],
parameters: {
// type-safe!
a11y: {
options: { xpath: true },
},
},
});
preview.metaThe meta function on the preview object is used to define the metadata for your stories. It accepts an object containing the component, title, parameters, and other story properties.
import preview from '../.storybook/preview';
import { Button } from './Button';
const meta = preview.meta({
component: Button,
parameters: {
// type-safe!
layout: 'centered',
}
});
If you would like to use absolute imports instead of relative imports for your preview config, like below, you can configure that using subpath imports or an alias.
// โ
Absolute imports won't break if you move story files around
import preview from '#.storybook/preview';
// โ Relative imports can break if you move story files around
import preview from '../../../.storybook/preview';
Subpath imports are a Node.js standard that allows you to define custom import paths in your project, which you can then use throughout your codebase.
To configure subpath imports, add the following to your package.json:
{
"imports": {
"#*": ["./*", "./*.ts", "./*.tsx"],
},
}
For more information, refer to the subpath imports documentation.
Alternatively, you can configure an alias in your builder (Vite or Webpack).
</details> </Callout>preview.type.metaBy default, preview.meta will infer the type of your component's props automatically. However, if you need to extend or modify the inferred types, you can use the preview.type function to specify custom types.
This example is for React, but the same API applies to all supported renderers.
import * as React from 'react';
import preview from '../.storybook/preview';
import { Button } from './Button';
type CustomProps = React.ComponentProps<typeof Button> & { customProp: string };
const meta = preview.type<{ args: CustomProps }>().meta({
component: Button,
// ๐ Correct types
render: ({ customProp, ...args }) => <>{customProp} <Button {...args} /></>,
});
export const Primary = meta.story({
// ๐ Correct types
args: {
customProp: 'Hello, world!',
},
});
For more information on typing your stories, refer to the writing stories in TypeScript guide.
meta.storyFinally, the story function on the meta object defines the stories. This function accepts an object containing the name, args, parameters, and other story properties.
// ...from above
const meta = preview.meta({ /* ... */ });
export const Primary = meta.story({
args: {
primary: true,
},
});
<Story>.extendYou can use the .extend method to create a new story based on an existing one, with the option to override or add new properties.
Properties are merged intelligently:
args are shallow mergedparameters are deep merged, except for arrays, which are replaceddecorators and tags are concatenated// ...from above
const meta = preview.meta({ /* ... */ });
export const Example = meta.story({
args: {
primary: true,
exampleArray: ['a', 'b'],
exampleObject: { a: 'a', b: 'b' },
},
parameters: {
exampleArray: ['a', 'b'],
exampleObject: { a: 'a', b: 'b' },
},
tags: ['a'],
});
/*
* ๐ Final values applied:
* {
* args: {
* primary: true,
* disabled: true,
* exampleArray: ['c'],
* exampleObject: { a: 'c' }
* },
* parameters: {
* exampleArray: ['c'],
* exampleObject: { a: 'c', b: 'b' }
* },
* tags: ['a', 'b']
* }
*/
export const ExtendedExample = Example.extend({
args: {
disabled: true,
exampleArray: ['c'],
exampleObject: { a: 'c' },
},
parameters: {
disabled: true,
exampleArray: ['c'],
exampleObject: { a: 'c' },
},
tags: ['b'],
});
// ...from above
const meta = preview.meta({ /* ... */ });
export const Primary = meta.story({
args: {
primary: true,
},
});
export const PrimaryDisabled = Primary.extend({
args: {
disabled: true,
},
});
<Story>.test(โ ๏ธ Experimental)
A more ergonomic way to define tests for your stories. While this API is still experimental, it is documented in the RFC to gather feedback and must be enabled via the experimentalTestSyntax feature flag.
// ...from above
export const PrimaryDisabled = Primary.extend({ args: { disabled: true } });
// ๐ .test method: Attach tests to a story
// The test function can run the same code as the play function
PrimaryDisabled.test('should be disabled', async ({ canvas, userEvent, args }) => {
const button = await canvas.findByRole('button');
await userEvent.click(button);
await expect(button).toHaveAttribute('aria-disabled', 'true');
await expect(args.onClick).not.toHaveBeenCalled();
});
You can upgrade your stories to CSF Next either automatically (from CSF 3) or manually (from CSF 1, 2, or 3). CSF Next is designed to be usable incrementally; you do not have to upgrade all of your story files at once. However, you cannot mix story formats within the same file.
Before proceeding, be sure you're using the latest version of Storybook. You can upgrade your Storybook automatically with this command:
<CodeSnippets path="storybook-upgrade.md" />You can automatically upgrade all of your project's stories from CSF 3 to CSF Next with this command:
<CodeSnippets path="csf-factories-automigrate.md" />If your project has multiple Storybook configurations, run the command with the -c flag pointing to each config directory:
It will run through each of the manual upgrade steps below on all of your story files.
<details> <summary>Upgrading from CSF 2 to 3</summary>You must be using CSF 3 to automatically upgrade to CSF Next. If you are using CSF 2, you can upgrade to CSF 3 first using this command:
<CodeSnippets path="migrate-csf-2-to-3.md" /> </details>You can also upgrade your project's story files to CSF Next manually. Before using CSF Next in a story file, you must upgrade your .storybook/main.js|ts and .storybook/preview.js|ts files.
1. Update your main Storybook config file
Update your .storybook/main.js|ts file to use the new defineMain function.
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
+ import { defineMain } from '@storybook/your-framework/node';
- import { StorybookConfig } from '@storybook/your-framework';
+ export default defineMain({
- export const config: StorybookConfig = {
// ...config itself is unchanged
+ });
- };
- export default config;
2. Update your preview config file <a name="preview-addons" />
Update your .storybook/preview.js|ts file to use the new definePreview function.
The ability for an addon to provide annotation types (parameters, globals, etc.) is new and not all addons support it yet.
If an addon provides annotations (i.e. it distributes a ./preview export), it can be imported in two ways:
For official Storybook addons, you import the default export:
import addonName from '@storybook/addon-name'
For community addons, you should import the entire module and access the addon from there:
import * as addonName from 'community-addon-name'
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
+ import { definePreview } from '@storybook/your-framework';
- import type { Preview } from '@storybook/your-framework';
// ๐ Import the addons you are using
+ import addonA11y from '@storybook/addon-a11y';
+ export default definePreview({
- export const preview: Preview = {
// ...current config
// ๐ Add your addons here
+ addons: [addonA11y()],
+ });
- };
- export default preview;
3. Update your story files
Story files have been updated for improved usability. With the new format:
preview.meta function and does not have to be exported as a default exportmeta.story function+ import preview from '../.storybook/preview';
- import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
+ const meta = preview.meta({
- const meta = {
// ...meta object is unchanged
+ });
- } satisfies Meta<typeof Button>;
- export default meta;
- type Story = StoryObj<typeof meta>;
+ export const Primary = meta.story({
- export const Primary: Story = {
// ...story object is unchanged
+ });
- };
Note that importing or manually applying the component props type to the meta or stories is no longer necessary. Thanks to the factory function pattern, the types are now inferred automatically.
<details> <summary>Handling complex types</summary>If you were previously using complex types for your meta or stories, such as intersection types to add custom args, you can now use the preview.type.meta pattern to explicitly specify types.
+ import preview from '../.storybook/preview';
- import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
type CustomProps = React.ComponentProps<typeof Button> & { customProp: string };
+ const meta = preview.type<{ args: CustomProps }>().meta({
- const meta = {
// ...meta object is unchanged
+ });
- } satisfies Meta<CustomProps>;
- export default meta;
- type Story = StoryObj<typeof meta>;
+ export const Primary = meta.story({
- export const Primary: Story = {
// ...story object is unchanged
+ });
- };
3.1 Reusing story properties
<Callout variant="info" icon="๐ก">If you are reusing story properties to create a new story based on another, the <Story>.extend method is the recommended way to do so.
Previously, story properties such as Story.args or Story.parameters were accessed directly when reusing them in another story. While accessing them like this is still supported, it is deprecated in CSF Next.
All of the story properties are now contained within a new property called composed and should be accessed from that property instead. For instance, Story.composed.args or Story.composed.parameters.
// ...rest of file
+ export const Primary = meta.story({
- export const Primary: Story = {
args: { primary: true },
+ });
- };
+ export const PrimaryDisabled = meta.story({
- export const PrimaryDisabled: Story = {
args: {
+ ...Primary.composed.args,
- ...Primary.args,
disabled: true,
}
+ });
- };
If you want to access the direct input to the story, you can use Story.input instead of Story.composed.
</Callout>
4. Update your Vitest setup file
If you're using Storybook's Vitest addon, you can remove your Vitest setup file (.storybook/vitest.setup.ts).
If you are using portable stories in Vitest, you may use a Vitest setup file to configure your stories. This file must be updated to use the new CSF Next format.
<Callout variant="warning"> Note that this only applies if you use CSF Next for all your tested stories. If you use a mix of CSF 1, 2, or 3 and CSF Next, you must maintain two separate setup files. </Callout>import { beforeAll } from 'vitest';
// ๐ No longer necessary
- // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { setProjectAnnotations } from '@storybook/your-framework';
- import * as addonAnnotations from 'my-addon/preview';
+ import preview from './.storybook/preview';
- import * as previewAnnotations from './.storybook/preview';
// No longer necessary
- const annotations = setProjectAnnotations([previewAnnotations, addonAnnotations]);
// Run Storybook's beforeAll hook
+ beforeAll(preview.composed.beforeAll);
- beforeAll(annotations.beforeAll);
5. Reusing stories in test files
Storybook's Vitest addon allows you to test your components directly inside Storybook. All the stories are automatically turned into Vitest tests, making integration seamless in your testing suite.
If you cannot use Storybook Test, you can still reuse the stories in your test files using portable stories. In prior story formats, you had to compose the stories before rendering them in your test files. With CSF Next, you can now reuse the stories directly.
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
- import { composeStories } from '@storybook/your-framework';
// Import all stories from the stories file
import * as stories from './Button.stories';
+ const { Primary } = stories;
- const { Primary } = composeStories(stories);
test('renders primary button with default args', async () => {
// The run function will mount the component and run all of Storybook's lifecycle hooks
await Primary.run();
const buttonElement = screen.getByText('Text coming from args in stories file!');
expect(buttonElement).not.toBeNull();
});
The Story object also provides a Component property, enabling you to render the component with any method you choose, such as Testing Library. You can also access its composed properties (args, parameters, etc.) via the composed property.
Here's an example of how you can reuse a story in a test file by rendering its component: <CodeSnippets path="portable-stories-csf-factory-render.md" />
Storybook will continue to support CSF 1, CSF 2, and CSF 3 for the foreseeable future. None of these prior formats are deprecated.
While using CSF Next, you can still use the older formats, as long as they are not mixed in the same file. If you want to migrate your existing files to the new format, refer to the upgrade section, above.
Yes, the doc blocks used to reference stories in MDX files support the CSF Next format with no changes needed.
For more information on this experimental format's original proposal, refer to its RFC on GitHub. We welcome your comments!
</If>