Storybook
Storybook is a workshop for building, documenting, and visually testing UI components outside the running application. Instead of clicking through five screens to reach a particular button state, you render that state directly as a story — an isolated, interactive example with controls you can tweak live. For Angular teams it doubles as living documentation, a visual regression baseline, and a design-system catalog that designers and developers share. This page covers setting Storybook up with modern standalone components, signals, and the new control flow.
Installing Storybook
Storybook ships an init command that detects your Angular workspace, installs the @storybook/angular framework package, and wires up the build. Run it from the project root.
npx storybook@latest init
This generates a .storybook/ folder, adds a storybook target to angular.json, and drops a few sample stories. Start the dev server with the npm script it created.
npm run storybook
Output:
@storybook/cli v8.4.0
info => Starting manager..
info => Starting preview..
╭──────────────────────────────────────────────╮
│ Storybook 8.4.0 for angular started │
│ Local: http://localhost:6006/ │
╰──────────────────────────────────────────────╯
The core config lives in .storybook/main.ts, where you declare where stories live and which addons load.
import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|mdx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
framework: { name: '@storybook/angular', options: {} },
};
export default config;
Writing your first story
A story file exports a Meta object describing the component plus one or more StoryObj exports for individual states. Because the button below is a standalone component, no NgModule wiring is needed — Storybook renders it directly.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-button',
standalone: true,
template: `
<button class="btn" [class.btn-primary]="primary()" [disabled]="disabled()">
{{ label() }}
</button>
`,
})
export class ButtonComponent {
label = input.required<string>();
primary = input(false);
disabled = input(false);
}
The matching story file uses the Component Story Format (CSF). args map directly onto the component’s signal inputs.
import type { Meta, StoryObj } from '@storybook/angular';
import { ButtonComponent } from './button.component';
const meta: Meta<ButtonComponent> = {
title: 'Components/Button',
component: ButtonComponent,
tags: ['autodocs'],
argTypes: {
label: { control: 'text' },
disabled: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj<ButtonComponent>;
export const Primary: Story = {
args: { label: 'Save changes', primary: true },
};
export const Disabled: Story = {
args: { label: 'Save changes', disabled: true },
};
The autodocs tag generates a documentation page from your argTypes, and each args value becomes an editable control in the Controls panel.
Providing dependencies and decorators
Real components inject services, use routing, or rely on HttpClient. Decorators let you supply providers and wrap stories in extra markup. Use applicationConfig to register providers via Angular’s standalone inject() model, and moduleMetadata for imported components or pipes.
import type { Meta } from '@storybook/angular';
import { applicationConfig, moduleMetadata } from '@storybook/angular';
import { provideHttpClient } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { UserCardComponent } from './user-card.component';
const meta: Meta<UserCardComponent> = {
title: 'Components/UserCard',
component: UserCardComponent,
decorators: [
applicationConfig({
providers: [provideHttpClient(), provideAnimations()],
}),
moduleMetadata({ imports: [] }),
],
};
export default meta;
Tip: Put cross-cutting providers (HttpClient, animations, a router) in
.storybook/preview.tswith a globalapplicationConfigdecorator so every story inherits them without repetition.
Interaction testing with the play function
Beyond static rendering, a play function runs after the story mounts and lets you script user behavior with @storybook/test. This turns a story into an executable test that you can also run headlessly in CI.
import type { Meta, StoryObj } from '@storybook/angular';
import { expect, userEvent, within } from '@storybook/test';
import { LoginFormComponent } from './login-form.component';
const meta: Meta<LoginFormComponent> = {
title: 'Forms/LoginForm',
component: LoginFormComponent,
};
export default meta;
type Story = StoryObj<LoginFormComponent>;
export const SubmitsWhenValid: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText('Email'), '[email protected]');
await userEvent.type(canvas.getByLabelText('Password'), 'hunter2');
await userEvent.click(canvas.getByRole('button', { name: /sign in/i }));
await expect(canvas.getByRole('status')).toHaveTextContent('Welcome');
},
};
Run the whole suite — including a11y checks and play functions — from the command line with the test runner.
npm run test-storybook
Addons at a glance
| Addon | Purpose |
|---|---|
addon-essentials | Controls, actions, docs, viewport, backgrounds |
addon-a11y | Flags accessibility violations per story |
addon-interactions | Step-through debugger for play functions |
addon-themes | Toggle light/dark or design tokens globally |
@chromatic-com/storybook | Cloud visual regression snapshots |
Best practices
- Co-locate
*.stories.tsnext to the component so stories evolve with the code they document. - Drive every story from
argsrather than hardcoded templates, so Controls stay live andautodocsproduces accurate tables. - Hoist shared providers and theme decorators into
.storybook/preview.tsinstead of repeating them per story. - Enable
@storybook/addon-a11yearly — catching contrast and ARIA issues in isolation is far cheaper than auditing full pages. - Write a
playfunction for any stateful component so a story doubles as an interaction test in CI. - Wire Chromatic or the test runner into your pipeline to catch visual regressions before they reach review.