Storybook CSF Factory and Meta Interface#
Overview#
The CSF Factory pattern (also called CSF4 / "CSF Next") is Storybook's modern API for defining stories. It replaces direct default-export objects with a fluent factory chain: definePreview β preview.meta() β meta.story(). This gives framework-aware, end-to-end type inference across the full story hierarchy.
The three core interfaces live in code/core/src/csf/csf-factories.ts:
Preview<TRenderer>β the top-level object created bydefinePreview. Holds project-level annotations and exposes ameta()factory method.Meta<TRenderer, TMetaInput>β the component-level object created bypreview.meta(). HoldsComponentAnnotationsand exposesstory().Story<TRenderer, TInput>β the story-level object created bymeta.story(). HoldsStoryAnnotationsand exposes.run(),.extend(), and.test().
A csfFactory: true parameter is injected into every meta's parameters at creation time so the runtime can identify CSF4 stories .
definePreview and Framework-Specific Wrappers#
Each framework exports its own definePreview (internally named __definePreview) that wraps the base function, prepends renderer-specific annotation modules (rendering, argTypes, docs), and returns a framework-typed Preview subinterface.
| Framework | Entry Point | Preview Type |
|---|---|---|
| React | code/renderers/react/src/preview.tsx | ReactPreview<T> |
| Vue 3 | code/renderers/vue3/src/preview.ts | VuePreview<T> |
| Web Components | code/renderers/web-components/src/preview.ts | WebComponentsPreview<T> |
| Angular (Vite) | code/frameworks/angular-vite/src/client/preview.ts | AngularPreview<T> |
Each renderer injects its annotation modules before user-provided addons .
The InferTypes<Addons> utility type merges addon-contributed arg/parameter/global types into TRenderer at compile time, propagating them through the entire chain.
Framework-Specific Meta Interfaces#
Each framework defines its own Meta sub-interface with three story() overloads:
- No-args render function β accepts
() => storyResultfor static stories that need no args. - Story annotations object β accepts
StoryAnnotations<T, T['args'], StoryArgs<T['args'], TMetaArgKeys>>, making any arg already provided in meta optional at the story level . - Zero-argument call β available only when
Partial<T['args']> extends StoryArgs<T['args'], TMetaArgKeys>(all required args satisfied by meta). Otherwise the overload resolves to[never], forcing a type error .
React's ReactMeta and ReactStory additionally expose a Component: ComponentType<Partial<T['args']>> property for portable-story compatibility .
How preview.meta() Types Args#
preview.meta() types the args object by which keys you provide, not by the values you write. This preserves meta args in story args even when props are typed as literal unions, enums, template literals, or branded types. Previously (before PR #36501), args set in meta were lost in these cases, requiring every story to repeat those args.
The Problem (Before)#
// Button props: { variant: 'primary' | 'secondary'; label: string }
const meta = preview.meta({ component: Button, args: { variant: 'primary' } });
export const Default = meta.story({ args: { label: 'Hi' } });
// β Previously: `variant` is required, even though it was set in meta
meta() inferred the whole args object in the same call that infers the props from component. 'primary' widened to string, the inferred type failed its Partial<Props> constraint, and TypeScript fell back to the constraint where every arg is optional.
The Fix (Now)#
// Button props: { variant: 'primary' | 'secondary'; label: string }
const meta = preview.meta({ component: Button, args: { variant: 'primary' } });
export const Default = meta.story({ args: { label: 'Hi' } });
// β
Now: `variant` is properly inherited from meta
meta() now infers only which keys you set. Each value is checked against the prop type through NoInfer, so it never takes part in inference and never widens. From those keys:
- A story must provide only the args meta didn't set (
StoryArgs<TArgs, TMetaArgKeys>). - A story receives the meta args as present. This applies in
play,render,loaders, the meta's own hooks, andmeta.input.args, so an optionalonClickset tofn()in meta is() => voidthere, not(() => void) | undefined.
The type logic lives in MetaArgs<TArgs, TKeys>, StoryArgs<TArgs, TKeys>, and WithMetaArgs<TArgs, TKeys> in code/core/src/csf/csf-factories.ts. Each renderer's meta() overload declares its inference sites (component, render). This change covers React, Vue 3, Angular, Angular-Vite, Web Components, TanStack React, and Next.js. There is no runtime change.
What Changed for Users#
- No more
as constfor literal args in meta. - Unknown keys in meta args are always an error. Declare extra args with
preview.type<{ args: β¦ }>(). - Use
mocked()to call mock methods on args. Args are now typed as the component declares them.Plain assertions like-args.getUsers.mockResolvedValue(users); +mocked(args.getUsers).mockResolvedValue(users);expect(args.onClick).toHaveBeenCalled()are unaffected.
This change closes issues #32829 and #36125.
ComponentAnnotations Constraint#
ComponentAnnotations<TRenderer, TArgs> is the base type for meta-level annotations. Two nuances worth knowing:
componentfield typing β uses(TRenderer & { T: TArgs })['component'], falling back toanywhenTArgsis a top type (Args,unknown, etc.) so a component with more specific props can still be assigned .- CSF4 conditional types β
globalsandparametersintersect additional core types whenTRenderer['csf4'] extends true.
The base Meta<TRenderer, TMetaArgKeys> interface now has a second type parameter TMetaArgKeys extends PropertyKey representing the union of meta arg keys. This replaced the previous second parameter that held the meta input type. Framework-specific Meta sub-interfaces (e.g., ReactMeta<T, TMetaArgKeys>, VueMeta<T, TMetaArgKeys>) follow the same pattern.
TypeScript 6 Compatibility#
The refactoring in PR #36501 removed the structural incompatibility between framework-specific Meta interfaces and the base Meta type that caused TypeScript 6 errors. Previously, framework-specific Meta sub-interfaces declared MetaInput extends ComponentAnnotations<T> (relying on the default second type parameter Args), while the base Meta required ComponentAnnotations<T, T['args']>. TypeScript 6's stricter constraint checking surfaced this as a hard error.
The new implementation uses TMetaArgKeys extends PropertyKey as the second type parameter, which both the base Meta<TRenderer, TMetaArgKeys> and framework-specific interfaces (e.g., ReactMeta<T, TMetaArgKeys>) now share consistently. The per-renderer copies of type logic and the @ts-expect-error suppressions are gone.
Key Source Files#
| File | Purpose |
|---|---|
code/core/src/csf/csf-factories.ts | Base Preview, Meta, Story interfaces and definePreview / defineMeta / defineStory implementations |
code/core/src/csf/story.ts | ComponentAnnotations, StoryAnnotations, Renderer, AddonTypes type definitions |
code/renderers/react/src/preview.tsx | ReactPreview, ReactMeta, ReactStory |
code/renderers/vue3/src/preview.ts | VuePreview, VueMeta, VueStory |
code/renderers/web-components/src/preview.ts | WebComponentsPreview, WebComponentsMeta, WebComponentsStory |
code/frameworks/angular-vite/src/client/preview.ts | AngularPreview, AngularMeta, AngularStory |
code/renderers/react/src/types.ts | ReactTypes, ReactRenderer, ReactParameters |