docs/get-started/frameworks/angular-vite.mdx
Storybook for Angular with Vite is a framework that makes it easy to develop and test UI components in isolation for Angular applications. It uses Vite for faster builds, better performance, and Storybook Testing support. The Vite transform pipeline is powered by the AnalogJS Vite plugin.
<Callout variant="info" icon="🧪">@storybook/angular-vite is currently in preview and is planned to be marked stable in Storybook 11. The framework is feature-complete for the documented use cases, but APIs and defaults may change based on feedback before then. Please report issues and share feedback on GitHub.
To install Storybook in an existing Angular project, run this command in your project's root directory:
<CodeSnippets path="create-command.md" variant="new-users" copyEvent="CreateCommandCopy" />You can then get started writing stories, running tests and documenting your components. For more control over the installation process, refer to the installation guide.
<GetStartedVersions versions={[ { name: 'Angular', range: '≥ 21', icon: '/images/logos/renderers/logo-angular.svg', }, { name: 'Vite', range: '≥ 8', icon: '/images/logos/builders/vite.svg', }, ]} />
@storybook/angular-vite is the Vite-based Angular framework. Use it when you want:
@storybook/angular frameworkUse @storybook/angular (Webpack 5) if your project requires Angular ≤ 20 or has custom Webpack configurations you cannot migrate.
You can run Storybook either with the standard Storybook CLI or, like @storybook/angular, through Angular CLI builders. Both paths share the same configuration.
To build:
<CodeSnippets path="build-storybook-production-mode.md" />The output lands in the configured outputDir (default storybook-static).
Register the start-storybook and build-storybook builders in angular.json:
{
"projects": {
"your-project": {
"architect": {
"storybook": {
"builder": "@storybook/angular-vite:start-storybook",
"options": {
"configDir": ".storybook",
"port": 6006,
},
},
"build-storybook": {
"builder": "@storybook/angular-vite:build-storybook",
"options": {
"configDir": ".storybook",
"outputDir": "dist/storybook/your-project",
},
},
},
},
},
}
Then run them with ng run your-project:storybook and ng run your-project:build-storybook.
Unlike the Webpack-based @storybook/angular, these builders do not take a browserTarget. Vite resolves your project's TypeScript and assets directly, so no Angular build target reference is required. Builder schemas live alongside the source: start-storybook, build-storybook.
The authoring surface (stories, decorators, parameters, moduleMetadata, and applicationConfig) is identical to @storybook/angular. Existing stories migrate without changes. Component documentation is the one place the two frameworks differ. The sections below describe every configuration option available in this framework.
JSDoc comments above your components, and above their @Input and @Output members, become descriptions in automatic documentation and in the controls table.
Two engines can read them, and which one you get depends on a single feature flag:
experimentalDocgenServer on, the default here | flag off | |
|---|---|---|
| Reads your components | on the Storybook server, from your TypeScript sources | in the browser, from Compodoc output |
| Compodoc | never runs, and is not a dependency | runs on demand |
documentation.json | never generated, never read | generated and read |
| Powers | controls, autodocs, code snippets, and the components manifest | controls and autodocs |
@storybook/angular-vite turns the flag on from its own configuration, so server-side extraction is what you get unless you turn it off. The Webpack-based @storybook/angular is unaffected and always uses Compodoc.
The flag-off column is temporary. It exists so that a project upgrading to 10.6 can keep the setup it already has while it migrates, and it is planned to be deprecated in Storybook 11 and removed in Storybook 12. From then on @storybook/angular-vite reads your components from source and nothing else. Plan on the default path rather than opting out.
Nothing to set up. npx storybook@latest init neither installs Compodoc nor writes any Compodoc wiring for this framework, and there is no documentation.json to generate or keep current.
Because your sources are read directly, editing a component updates its controls and descriptions without restarting Storybook.
See Known limitations for what this path does not yet do.
This path is a migration aid, not a supported long-term configuration. Turning experimentalDocgenServer off on @storybook/angular-vite is planned to be deprecated in Storybook 11 and removed in Storybook 12, along with the compodoc and compodocArgs framework options and the setCompodocJson wiring. Use it to keep an existing project working while you migrate, and plan on the default path.
Turn the feature off in your .storybook/main.ts:
import type { StorybookConfig } from '@storybook/angular-vite';
const config: StorybookConfig = {
framework: '@storybook/angular-vite',
features: {
experimentalDocgenServer: false,
},
};
export default config;
Then install Compodoc:
npm install --save-dev @compodoc/compodoc
And hand its output to the preview:
import { setCompodocJson } from '@storybook/addon-docs/angular';
import docJson from '../documentation.json';
setCompodocJson(docJson);
Storybook generates documentation.json itself, once per run: every storybook dev, every storybook build, and every Vitest run. Compodoc scans the whole project in one pass, so this is all-or-nothing: there is no per-component regeneration.
.compodoc.lock while a run is in progress, and .compodoc.run recording which run produced the current documentation. Together they let several Storybook processes share a single Compodoc run instead of each starting their own. Both are safe to add to your .gitignore.documentation.json is published into the output directory. If your compodocArgs also produce Compodoc's browsable HTML site, generate that with a separate compodoc invocation.Generation is controlled by the compodoc and compodocArgs framework options.
npx storybook automigrate removes the setup that no longer does anything: the compodoc and compodocArgs framework options, the setCompodocJson call and the imports that fed it, the same two options on your angular.json Storybook targets, and the @compodoc/compodoc dependency. It skips any project that sets experimentalDocgenServer: false.
If you keep the wiring instead of running the automigration, nothing breaks:
setCompodocJson returns without storing anything and logs a warning once per session, so a stale documentation.json can never reach the controls table.import docJson from '../documentation.json' that storybook init used to write resolves to an empty object when the file is genuinely absent, so your preview still builds. This applies only to that import inside your Storybook configuration folder; a documentation.json you import anywhere else is your own file and still fails to resolve if it is missing.ng run app:storybook: Uses angular.json for Angular build settings. With Compodoc enabled, the framework runs it at start-up so documentation.json is generated before stories render.storybook dev): The addon-vitest child process inherits builder options from the parent process automatically; no extra configuration is needed.yarn vitest (without a parent storybook dev): Supported via storybookAngularVitest from @storybook/angular-vite/vitest. Add it to the same plugins array as storybookTest in your vitest.config.ts; it forwards your Angular build options (styles, stylePreprocessorOptions, assets, zoneless) into the channel the framework already reads. If the env var is already set (e.g. a parent storybook dev is running), the existing value wins and a warning is logged so you know which options are active.If your component relies on application-wide providers (such as those returned by provide-style functions or set up by any module using the forRoot pattern), apply the applicationConfig decorator to supply them via the bootstrapApplication function.
If your component has dependencies on other Angular directives and modules, supply them using the moduleMetadata decorator either for all stories of a component or for individual stories.
By default, this framework runs with zoneless change detection (zoneless: true). To opt into Zone.js-based change detection, set the zoneless option to false on the Storybook builder target in your angular.json:
"storybook": {
"builder": "@storybook/angular-vite:start-storybook",
"options": {
"zoneless": false,
},
},
When zoneless is false, zone.js is automatically imported at the start of the preview.
You can extend the Vite configuration used by Storybook in your .storybook/main.ts file via viteFinal:
import type { StorybookConfig } from '@storybook/angular-vite';
const config: StorybookConfig = {
framework: '@storybook/angular-vite',
async viteFinal(config) {
const { mergeConfig } = await import('vite');
return mergeConfig(config, {
// your overrides
});
},
};
export default config;
Unlike the Webpack-based @storybook/angular, this framework does not automatically map your tsconfig.json paths aliases into Vite's module resolver. Vite resolves modules on disk and does not read the paths compiler option, so imports such as @app/shared will fail unless you register them yourself.
The simplest fix is the vite-tsconfig-paths plugin, which reads baseUrl and paths from your tsconfig.json and adds the matching aliases:
import type { StorybookConfig } from '@storybook/angular-vite';
const config: StorybookConfig = {
framework: '@storybook/angular-vite',
async viteFinal(config) {
const { mergeConfig } = await import('vite');
const { default: tsconfigPaths } = await import('vite-tsconfig-paths');
return mergeConfig(config, {
plugins: [tsconfigPaths()],
});
},
};
export default config;
Install it as a dev dependency first: npm install --save-dev vite-tsconfig-paths. If you prefer not to add a plugin, you can instead declare the aliases explicitly under resolve.alias in the same viteFinal hook.
Like paths, SCSS search paths from your application's build target in angular.json are not inherited. Configure them on the Storybook builder target instead, using stylePreprocessorOptions. Both the Angular-style includePaths and the dart-sass/Vite spelling loadPaths are accepted, and paths are resolved relative to the workspace root:
"storybook": {
"builder": "@storybook/angular-vite:start-storybook",
"options": {
"stylePreprocessorOptions": {
"includePaths": ["src/styles"],
},
},
},
When running through the Storybook CLI (storybook dev / storybook build) rather than the Angular builders, there is no angular.json builder context to read from. In that case set the search paths directly in .storybook/main.ts:
async viteFinal(config) {
const { mergeConfig } = await import('vite');
return mergeConfig(config, {
css: { preprocessorOptions: { scss: { loadPaths: ['src/styles'] } } },
});
},
These apply to the default server-side extraction path, which is what you get unless you turn the feature off. They are not opt-in: if you use @storybook/angular-vite, they apply to you.
The snippets shown in autodocs and the Code panel are generated from your story's source, so they show the story as you wrote it. Changing a value in the controls table updates the rendered component but not the snippet next to it.
T | undefined falls back to an object controlAn input whose type explicitly includes undefined is not narrowed to its members, so the controls table offers an object editor rather than the radio buttons or select you would expect:
export type BadgeVariant = 'accent' | 'danger' | 'primary';
@Component({ selector: 'sb-badge', template: '' })
export class BadgeComponent {
@Input() withUndefined: BadgeVariant | undefined = undefined; // object editor
@Input() optional?: BadgeVariant; // radio buttons, as expected
}
Marking the input optional with ? instead of widening its type gives you the control you want. Failing that, you can declare the control yourself in argTypes. Either way the full type still appears in the props table, so the information is lost from the control only.
@storybook/angularRun the Storybook automigration command to update your project automatically:
npx storybook automigrate
Two things the automigration cannot do for you: a webpackFinal hook has to be rewritten as viteFinal, and direct .md imports need Vite's ?raw suffix.
First, install the framework:
<CodeSnippets path="angular-vite-install.md" />Then, update your .storybook/main.ts to change the framework property:
If your existing angular.json already declares Storybook architect targets, update the builder references to use the new framework and drop the browserTarget option (see why):
{
"projects": {
"your-project": {
"architect": {
"storybook": {
- "builder": "@storybook/angular:start-storybook",
+ "builder": "@storybook/angular-vite:start-storybook",
"options": {
- "browserTarget": "your-project:build",
//... other options
},
},
"build-storybook": {
- "builder": "@storybook/angular:build-storybook",
+ "builder": "@storybook/angular-vite:build-storybook",
"options": {
- "browserTarget": "your-project:build",
//... other options
},
},
},
},
},
}
If you would rather invoke Storybook directly, you can also remove the architect entries entirely and switch to storybook dev and storybook build.
If your configuration contains a webpackFinal hook, you will need to migrate it to viteFinal.
Finally, if your stories or components import Markdown files directly, append Vite's ?raw suffix to those imports.
Webpack turned .md imports into strings for you; Vite needs to be told.
See Importing Markdown files as strings.
Because @storybook/angular-vite is a Vite-based framework, it supports the Vitest addon for running component tests directly inside Storybook.
Run the following command to install and configure the addon automatically:
<CodeSnippets path="addon-test-install.md" />This will install @storybook/addon-vitest, configure Vitest in browser mode using Playwright's Chromium browser, and set up the Vitest plugin. For Angular projects, the installer also scaffolds storybookAngularVitest({}) next to storybookTest() in your vitest.config.ts so standalone yarn vitest runs pick up your Angular build options automatically.
Refer to the Vitest addon guide for the full configuration reference.
yarn vitest (without Storybook dev)When you run yarn vitest outside of a running storybook dev, the storybookAngularVitest helper from @storybook/angular-vite/vitest forwards your Angular build options (styles, stylePreprocessorOptions, assets, zoneless) into the channel the framework reads. Place it in the same plugins array as storybookTest:
import { storybookAngularVitest } from '@storybook/angular-vite/vitest';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
projects: [
{
plugins: [
// Bridge Angular build options into standalone vitest runs.
// When a parent `storybook dev` is running, the existing env var
// wins and a warning is logged; options here are ignored in that run.
storybookAngularVitest({
// styles: ['src/styles.css'],
// stylePreprocessorOptions: { includePaths: ['src'] },
// assets: [{ glob: '**/*', input: 'src/assets', output: 'assets' }],
// zoneless: true,
}),
storybookTest({ configDir: '.storybook' }),
],
test: {
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
},
],
},
});
If you use a Vitest workspace file or a setup other than storybookTest(), follow the Analog Storybook integration docs instead.
Yes. @storybook/angular-vite ships start-storybook and build-storybook builders so you can run ng run your-project:storybook and ng run your-project:build-storybook. See Run Storybook → With the Angular CLI for the angular.json setup. You can also invoke Storybook directly with storybook dev / storybook build.
No. This framework requires Angular 21. For earlier Angular versions, use @storybook/angular.
Yes. The story format (CSF), decorators (moduleMetadata, applicationConfig, componentWrapperDecorator) and parameters are identical between @storybook/angular and @storybook/angular-vite. Stories files will only require changes to the framework import paths (handled automatically during migration). Your stories do not change, but where their documentation comes from does: this framework reads your components directly instead of through Compodoc.
@storybook/angular or @storybook/angular-vite?Use @storybook/angular-vite if you are on Angular 21 and want faster builds and the Vitest addon. Use @storybook/angular if you need Angular 18–20 or existing Webpack-based tooling you cannot migrate. Both frameworks support Angular CLI builders.
You can pass an options object for additional configuration:
<CodeSnippets path="angular-vite-framework-options.md" />The available options are:
builderType: Record<string, any>
Configure options for the framework's builder. Available options can be found in the Vite builder docs.
jitType: boolean
Default: true
Whether to use Angular's JIT compiler. Passed to the AnalogJS Vite plugin.
liveReloadType: boolean
Default: false
Whether to enable live-reload in the AnalogJS Vite plugin.
tsconfigType: string
Default: ./.storybook/tsconfig.json
Path to the TypeScript configuration file, relative to the workspace root. Passed to the AnalogJS Vite plugin.
inlineStylesExtensionType: string
Default: 'css'
File extension used for inline component styles. Passed to the AnalogJS Vite plugin.
compodocType: boolean
Default: true
Whether to run Compodoc to generate documentation.json. When true, the framework runs Compodoc once per Storybook run, replacing the previous documentation.json. Set to false to skip generation entirely (e.g. when you manage Compodoc outside of Storybook).
This option only takes effect with experimentalDocgenServer: false. On the default path Compodoc never runs whatever this is set to.
It controls the Compodoc run and nothing else. Setting it to false does not turn off component documentation: on the default path your components are still read from their TypeScript sources.
Planned to be deprecated in Storybook 11 and removed in Storybook 12, together with the opt-out it depends on.
compodocArgsType: string[]
Default: ['-e', 'json', '-d', '.']
Arguments passed to the @compodoc/compodoc CLI when compodoc is true. The defaults produce a documentation.json file in the workspace root.
Like compodoc, this is only read when experimentalDocgenServer is off, and is planned for removal on the same schedule.
propsTableType: 'all' | 'api' | 'inputs'
Default: 'api'
Which of your component's members the props table renders. The three values are a ladder, each one a subset of the one above it:
| Value | Renders |
|---|---|
'all' | Every member of every section: properties, inputs, outputs and methods. |
'api' | The same four sections, narrowed to your component's template-facing API. |
'inputs' | The inputs section only. |
'api' keeps every declared input and output, whatever its TypeScript visibility.
Everywhere else, 'api' drops TypeScript private members, ECMAScript private # members, and anything tagged @internal.
A private property or method cannot be reached from any template, and @internal declares a member non-API, so a row for them documents your component's wiring rather than its API.
Injected services are the common case:
@Component({ selector: 'my-button', template: '<button>{{ label }}</button>' })
export class ButtonComponent {
// Kept by 'api': a parent template can bind any declared input.
@Input() private label = '';
// Dropped by 'api': no template can reach it.
private readonly cdr = inject(ChangeDetectorRef);
// Kept by 'api': the component's own template can read protected members.
protected pressed = false;
}
protected members are deliberately kept, because Angular templates can bind them and they are therefore part of what a reader needs to know.
To drop a single member that 'api' keeps, tag it @ignore:
/** @ignore */
protected internalHelper = 0;
'api' needs the experimentalDocgenServer feature, which this framework turns on by default, so it works without any extra configuration.
If you have turned that feature off, Storybook reads your components through Compodoc, whose visibility data Storybook cannot interpret reliably, so only 'all' and 'inputs' apply.
Asking for 'api' then logs a warning rather than silently changing what you see.