Why I Built ng-prism — An Angular-Native Storybook Alternative With Zero Story Files
TL;DR: ng-prism lets you showcase Angular components by adding a single decorator to the component class itself. No story files, no parallel file tree, no framework mismatch. Just Angular. The Problem Every Angular Team Knows If you've ever maintained a Storybook setup for an Angular component library, you know the drill: for every component you write, you also write a .stories.ts file. Then you keep both in sync. Then someone renames an input, the stories break silently, and nobody notices until the designer opens Storybook two sprints later. Storybook is a fantastic tool — but it was born in the React ecosystem. Angular support has always been a second-class citizen. The CSF format doesn't feel natural in Angular. The iframe rendering breaks MatDialog, CDK overlays, and portals. The webpack/Vite configuration is yet another build system you have to understand alongside the Angular CLI. I wanted something different. Something that feels like Angular because it is Angular. Introducing ng-prism ng-prism is a lightweight component showcase tool built from the ground up for Angular. The core idea is radical in its simplicity: you annotate your component with a @Showcase decorator, and ng-prism discovers it at build time via the TypeScript Compiler API. No story files. No parallel file tree. The documentation lives where the code lives. import { Showcase } from '@ng-prism/core'; import { Component, input, output } from '@angular/core'; @Showcase({ title: 'Button', category: 'Atoms', description: 'The primary action button.', variants: [ { name: 'Primary', inputs: { label: 'Save', variant: 'primary' } }, { name: 'Danger', inputs: { label: 'Delete', variant: 'danger' } }, { name: 'Ghost', inputs: { label: 'Cancel', variant: 'ghost' } } ], }) @Component({ selector: 'lib-button', standalone: true, template: `<button [class]="variant()">{{ label() }}</button>` }) export class ButtonComponent { label = input.required<string>(); variant = input<'primary' | 'danger' | 'ghost'>('primary'); clicked = output<void>(); } That's it. Run ng run my-lib:prism, and you get a fully interactive styleguide with variant tabs, a live controls panel, event logging, and code snippets — all extracted from your actual component at build time. How It Works Under the Hood ng-prism doesn't guess your component's API. It reads it. At build time, a custom Angular Builder kicks off a pipeline: TypeScript Compiler API scanner — Parses your library's entry point, walks the AST, and extracts every component annotated with @Showcase. It reads input() and output() signal declarations, infers types, detects defaults, and builds a complete component manifest. Plugin hooks — Registered plugins can enrich the scanned data (e.g., extracting JSDoc comments, injecting Figma URLs from metadata). Runtime manifest generation — The pipeline produces a TypeScript file with real import statements pointing to your actual component classes. No JSON serialization, no runtime reflection. Angular Dev Server — The builder delegates to @angular-devkit/architect, so you get the Angular dev server you already know — HMR, source maps, the works. The result: your styleguide is a regular Angular app. No iframe. Components render in the same document context. MatDialog? Works. CDK Overlay? Works. CSS custom properties from a parent theme? Inherited naturally. ## Signal-Native From Day One ng-prism was built for Angular 21+ and the signal API. It understands input(), input.required(), and output() natively. The controls panel automatically generates the right control widget for each input type: string → text field boolean → toggle number → number input Union types like 'primary' | 'danger' → dropdown Complex types → JSON editor When you change a value in the controls panel, it flows through Angular's signal system. There's no @Input() decorator support — and that's by design. If you're starting a new component library in 2026, you should be using signals. ## Built-In Accessibility Auditing Accessibility isn't a plugin in ng-prism — it's a core feature. The built-in A11y panel provides four perspectives on every component: Violations — axe-core audit with a visual score ring, sorted by impact severity Keyboard Navigation — Tab order visualization with overlay indicators ARIA Tree — The accessibility tree as the browser sees it Screen Reader Simulation — Step through your component the way a screen reader would, with play/pause navigation Switch to "Screen Reader" perspective in the toolbar, and the canvas dims while SR annotations overlay your component. It's a first-class development tool, not an afterthought. ## Plugin Architecture — Extend Everything ng-prism follows a Vite-style plugin model. A plugin is a plain object with optional hooks for both build time and runtime: import type { NgPrismPlugin } from '@ng-prism/core/plugin'; export function myPlugin(): NgPrismPlugin { return { name: 'my-plugin', // Build-time: enrich scanned component data onComponentScanned(component) { component.meta = { ...component.meta, myData: extractSomething(component) }; }, // Runtime: add a panel to the UI panels: [{ id: 'my-panel', label: 'My Panel', loadComponent: () => import('./my-panel.component.js').then(m => m.MyPanelComponent), position: 'bottom', placement: 'addon', }], }; } Official Plugins @ng-prism/plugin-jsdoc — Extracts JSDoc comments at build time and generates structured API documentation, including parameter tables @ng-prism/plugin-figma — Embeds Figma designs as interactive iframes to enable direct visual comparison with components @ng-prism/plugin-box-model — Overlays CSS box model dimensions directly on rendered components for layout inspection @ng-prism/plugin-perf — Profiles initial render and re-render performance using the browser Performance API @ng-prism/plugin-coverage — Displays per-component test coverage based on Istanbul/V8 reports Plugins lazy-load their components, so they don't bloat your initial bundle. Setup in Two Minutes ng add @ng-prism/core The schematic asks which library you want to showcase, creates a prism app project, wires up the builder targets in angular.json, and generates a config file. Then: ng run my-lib:prism Your styleguide is running at localhost:4200. For the config, you get a typed prism.config.ts: import { defineConfig } from '@ng-prism/core'; import { jsDocPlugin } from '@ng-prism/plugin-jsdoc'; import { figmaPlugin } from '@ng-prism/plugin-figma'; export default defineConfig({ plugins: [ jsDocPlugin(), figmaPlugin() ], theme: { '--prism-primary': '#6366f1', '--prism-primary-from': '#6366f1', '--prism-primary-to': '#8b5cf6' // ... } }); Watch Mode The serve builder watches your library sources and config file. Change a component, add a @Showcase decorator, modify an input — the manifest regenerates, and the Angular dev server picks up the change. No restart needed. Component Pages & Custom Pages Not everything fits neatly into a per-component showcase. Sometimes you need a "Patterns" page showing how multiple components compose, or a color token overview. ng-prism supports Component Pages — free-form Angular components registered alongside your showcased components. They're defined in your main.ts via providePrism(): import { PrismShellComponent, providePrism, componentPage } from '@ng-prism/core'; import { ButtonPatternsPageComponent } from './pages/button-patterns.page.js'; bootstrapApplication(PrismShellComponent, { providers: [ providePrism(manifest, config, { componentPages: [ componentPage({ title: 'Button Patterns', category: 'Atoms', component: ButtonPatternsPageComponent, }), ], }), ], }); For static pages that don't need Angular components, use Custom Pages directly in prism.config.ts: export default defineConfig({ pages: [ { type: 'custom', title: 'Changelog', category: 'Meta', data: { version: '2.1.0' } }, ], }); Both appear in the sidebar navigation alongside regular components. Content Projection & Directive Hosting Real-world components use <ng-content>. ng-prism handles this with a content property on variants: @Showcase({ title: 'Card', variants: [{ name: 'With Header', content: { '[card-header]': '<h3>Title</h3>', 'default': '<p>Body content</p>' } }] }) Need to showcase a directive instead of a component? Use the host property to specify what element it attaches to — either a plain HTML string or another Angular component. Why Not Just Use Storybook? Storybook is mature, battle-tested, and has a massive ecosystem. If it works for your team, keep using it. But if you've felt the friction of: Maintaining a parallel .stories.ts file tree that drifts out of sync Fighting iframe restrictions when your component uses overlays or portals Configuring a separate build system (webpack/Vite) alongside the Angular CLI Wrapping Angular-specific patterns (dependency injection, signals) in framework-agnostic abstractions …then ng-prism might be worth a look. It doesn't try to be framework-agnostic. It's Angular, all the way down. The Road Ahead ng-prism is open source under the MIT license and follows Angular's versioning: @ng-prism/core@21.x targets Angular 21. The current release is v21.6.1. Why already v21.6.1? Because we already use it in my company for our component library, so it's already battle-tested. What's coming next: More official plugins CI integration — Manifest validation in your pipeline (e.g. ensuring every public component has a @Showcase decorator) Design token documentation — Automatic token overview extracted from SCSS / CSS custom properties Export — Share your component catalog as a PDF or static HTML page Get Started 🔗 Demo 📖 Docs 📦 npm install @ng-prism/core dyingangel666 / ng-prism Lightweight Angular-native component showcase — no story files needed. ng-prism Lightweight, Angular-native component showcase tool. Annotate components with @Showcase — no separate story files needed. Live Demo · Documentation Features Zero-config discovery — TypeScript Compiler API scans your library at build time Signal-native — works with input() / output() signals Directive support — showcase directives with configurable host elements Plugin architecture — JSDoc, A11y, Figma, Performance, Box Model, Coverage Live Controls — auto-generated input controls with type-aware editors Code Snippets — live-updating Angular template snippets per variant Component Pages — free-form demo pages for complex components Deep-linking — URL state sync for sharing specific component/variant/view Themeable — full CSS custom property system, replaceable UI sections Quick Start 1. Install npm install @ng-prism/core 2. Add @Showcase to a component import { Component, input, output } from '@angular/core'; import { Showcase } from '@ng-prism/core'; @Showcase({ title: 'Button', category: 'Atoms', description… View on GitHub </p> If you're building an Angular component library and want your showcase to feel like Angular — give ng-prism a try. Star the repo if you find it useful, and feel free to open issues or contribute plugins.
Loading comments…