<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex</title>
    <description>The latest articles on DEV Community by Alex (@alexanderspies).</description>
    <link>https://dev.to/alexanderspies</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909446%2Fef572afd-6e7d-408c-944c-eff3abe3637d.png</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/alexanderspies</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexanderspies"/>
    <language>en</language>
    <item>
      <title>Not everything in an Angular library is a component: what's new in ng-prism 22.7</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sat, 31 Oct 2026 18:29:40 +0000</pubDate>
      <link>https://dev.to/alexanderspies/not-everything-in-an-angular-library-is-a-component-whats-new-in-ng-prism-227-25jf</link>
      <guid>https://dev.to/alexanderspies/not-everything-in-an-angular-library-is-a-component-whats-new-in-ng-prism-227-25jf</guid>
      <description>&lt;p&gt;ng-prism 22.7 is out. If you haven't come across it yet: ng-prism is a component lab for Angular libraries, a Storybook alternative without story files. You put a &lt;code&gt;@Showcase&lt;/code&gt; decorator on a component, the TypeScript compiler picks up inputs, outputs and types at build time, and the components render in the same document as the styleguide, without an iframe. &lt;a href="https://dev.to/alexanderspies/what-an-angular-native-storybook-alternative-actually-looks-like-3jm3"&gt;The full tour&lt;/a&gt; has the details.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/p6pbJI2vWfs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Voice: elevenlabs.io&lt;/p&gt;

&lt;p&gt;This release got bigger than I planned. Most of it is about the parts of a library that aren't a single component: services, pipes, components that only work together with others, and components that only exist inside another one. Besides that the styleguide works on a phone now, it has a new look, accessibility violations can be accepted with a reason, and the perf plugin measures what you actually ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services and pipes get their own pages
&lt;/h2&gt;

&lt;p&gt;A real library usually has more than components. A service that shows notifications, a few pipes, provider functions that configure them. Until now none of that had a place in the styleguide. If you put &lt;code&gt;@Showcase&lt;/code&gt; on an &lt;code&gt;@Injectable&lt;/code&gt;, ng-prism treated it as a component and tried to render it, which never made much sense.&lt;/p&gt;

&lt;p&gt;In 22.7 services and pipes are their own kind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Time Ago&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Formatting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;provideTimeAgo&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeAgo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TimeAgoPipe&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;PipeTransform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They get their own sections in the sidebar, Services and Pipes, and their page has no canvas. There's nothing to render, so it's an API page: the description, a usage snippet you can copy, the examples from your JSDoc, the properties and methods of a service or the &lt;code&gt;transform()&lt;/code&gt; overloads of a pipe, and cards for the types they use. &lt;code&gt;{@link}&lt;/code&gt; and &lt;code&gt;@see&lt;/code&gt; jump to those cards.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setup&lt;/code&gt; is the part I care about most. It lists the provider functions and injection tokens a consumer needs, and the usage snippet calls them in &lt;code&gt;providers: [...]&lt;/code&gt;. So the page doesn't only tell you what the service has, it also tells you how to wire it up.&lt;/p&gt;

&lt;p&gt;Components and directives get the same API page now. Inputs link to their types, outputs show their payload, documented methods are listed, and there's an "On this page" list next to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ocqsxm8u1h6aoatvtm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ocqsxm8u1h6aoatvtm9.png" alt="the Time Ago pipe's API page (?pipe=TimeAgoPipe&amp;amp;view=jsdoc) with the usage snippet, the setup and the transform overloads." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have a look at the &lt;a href="https://ng-prism.github.io/ng-prism/demo/?service=NotificationService&amp;amp;view=jsdoc" rel="noopener noreferrer"&gt;Notification Service&lt;/a&gt; and the &lt;a href="https://ng-prism.github.io/ng-prism/demo/?pipe=TimeAgoPipe&amp;amp;view=jsdoc" rel="noopener noreferrer"&gt;Time Ago pipe&lt;/a&gt; in the demo.&lt;/p&gt;

&lt;p&gt;Two things to know. The pages come from the JSDoc plugin, so you need &lt;code&gt;jsDocPlugin()&lt;/code&gt; 22.7 or newer, otherwise services and pipes are only counted and you get a notice in the build. And a pipe has to be standalone with a readable &lt;code&gt;name&lt;/code&gt;, otherwise it's left out with a warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components that only work together
&lt;/h2&gt;

&lt;p&gt;Some components don't make sense on their own. A form field without an input is an empty frame, and an input directive without its form field is a bare &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;. So far the best you could do was project some HTML into the form field. The input in there was plain markup, not your directive, so the form field never found its control.&lt;/p&gt;

&lt;p&gt;In 22.7 a showcase can contain a real Angular template, and ng-prism compiles it. This is how the input directive in the demo shows up inside its form field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InputDirective&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;lib-form-field label="Name"&amp;gt;&amp;lt;input libInput type="text" /&amp;gt;&amp;lt;/lib-form-field&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;With hint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;lib-form-field label="Name" hint="As on your ID"&amp;gt;&amp;lt;input libInput /&amp;gt;&amp;lt;/lib-form-field&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The directive is the subject, and the form field around it is real. Content queries and DI work the way they do in your app, so the form field finds its input like it would in a form. The controls still belong to the subject.&lt;/p&gt;

&lt;p&gt;There are three fields for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;template&lt;/code&gt; on a variant is the content of the subject, compiled instead of pasted in as HTML.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;host: { template }&lt;/code&gt; puts the subject inside other markup. If more than one element matches, &lt;code&gt;prism-subject&lt;/code&gt; marks the one you mean.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;imports&lt;/code&gt; brings in classes from outside your library. Classes from your own library are imported automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A related fix: a directive without a &lt;code&gt;host&lt;/code&gt; now renders on the element its selector names. &lt;code&gt;input[libInput]&lt;/code&gt; becomes &lt;code&gt;&amp;lt;input libInput&amp;gt;&lt;/code&gt;, and void elements like &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; no longer break the build.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqorml5tbb4sfbzxu10m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqorml5tbb4sfbzxu10m.png" alt="the Input directive inside its form field (?component=InputDirective), the &amp;quot;Error&amp;quot; variant." width="799" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The limits: expressions in these templates have no context, so only literals and template reference variables work. Overlays that render into &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; aren't part of the composition. And if you have visual regression baselines for directives, look at them after the update, because some directives now render on a different element than before.&lt;/p&gt;

&lt;p&gt;In the demo: the &lt;a href="https://ng-prism.github.io/ng-prism/demo/?component=InputDirective" rel="noopener noreferrer"&gt;Input directive&lt;/a&gt; and the &lt;a href="https://ng-prism.github.io/ng-prism/demo/?component=FormFieldComponent" rel="noopener noreferrer"&gt;Form Field&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parts
&lt;/h2&gt;

&lt;p&gt;The opposite case are components that only ever live inside another one, like a list item or an option in a select. Their own page would show a list item floating alone on the canvas, which helps nobody.&lt;/p&gt;

&lt;p&gt;Now such a component can say whose part it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ListItemComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List Item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;partOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib-list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;One entry of a lib-list.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A part has no page of its own. The parent's head shows a Parts chip that opens a card with the part's inputs and outputs, and the parent's API tab gets a Parts section. If somebody opens the part's old URL, they land on the parent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;partOf&lt;/code&gt; takes the parent's selector as a string, not the class. That way the part doesn't have to import its parent, which would end in an import cycle pretty quickly. A part has exactly one parent, there are no chains.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ng-prism.github.io/ng-prism/demo/?component=ListComponent&amp;amp;view=jsdoc&amp;amp;part=ListItemComponent" rel="noopener noreferrer"&gt;The List with its item&lt;/a&gt; in the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The styleguide on a phone
&lt;/h2&gt;

&lt;p&gt;Until now the styleguide was hard to use on a phone, which was annoying whenever somebody sent a link to a component. Up to 900 px width, and on touch screens up to 500 px height (a phone in landscape), the layout changes now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The navigation opens full screen from a menu button.&lt;/li&gt;
&lt;li&gt;The canvas gets the whole width, and the panel sits below it at about half the height. Tap the active tab and it folds away, drag the grip to resize it.&lt;/li&gt;
&lt;li&gt;The header keeps what matters, the theme switch and the build info move into the menu.&lt;/li&gt;
&lt;li&gt;Controls stack, touch targets are at least 40 px, and text fields use 16 px so iOS doesn't zoom in when you tap them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The measuring tools stay out on a phone. Guides, rulers, the measuring tool and the viewport width need a mouse and a bigger screen to be useful.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftpgjmz77ephsyicxa92o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftpgjmz77ephsyicxa92o.png" alt="the Button in the demo on a phone, panel open." width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The easiest way to try it is to open the &lt;a href="https://ng-prism.github.io/ng-prism/demo/" rel="noopener noreferrer"&gt;demo&lt;/a&gt; on your phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accepting an accessibility violation
&lt;/h2&gt;

&lt;p&gt;ng-prism runs axe on every variant. Sometimes a violation is known and you've decided to keep it, for now or for good. So far the only option was to disable the rule, and then it disappeared from the panel. In 22.7 you can accept it, with a reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;a11y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;color-contrast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The hint grey (#6b7280) reaches 4.47:1 on the light ground, a hair under the 4.5:1 WCAG AA asks of small text.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is required. Accepted violations are still audited and listed under Accepted with their reason, they just don't count against the score. That's the difference to &lt;code&gt;disable&lt;/code&gt;: you still see them, and so does the next person who looks at the component. &lt;code&gt;meta.a11y&lt;/code&gt; is typed now too, so &lt;code&gt;rules&lt;/code&gt;, &lt;code&gt;disable&lt;/code&gt; and &lt;code&gt;accept&lt;/code&gt; are checked by the compiler.&lt;/p&gt;

&lt;p&gt;What axe can't decide on its own now shows up under Needs review instead of being dropped, and it doesn't count either. And the build gate is its own option, &lt;code&gt;a11y.gate&lt;/code&gt;. If you leave it out, it's your thresholds like before. &lt;code&gt;gate: false&lt;/code&gt; never fails the build, which is handy when your CI does the gating and the styleguide should still show the real numbers. The dev server doesn't stop over the gate anymore, it logs a warning and rebuilds when a new report comes in.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ng-prism.github.io/ng-prism/demo/?component=FormFieldComponent&amp;amp;variant=5&amp;amp;panel=a11y" rel="noopener noreferrer"&gt;Form Field's "With hint" variant&lt;/a&gt; in the demo has an accepted violation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new look
&lt;/h2&gt;

&lt;p&gt;The colours changed. The primary colour is blue instead of violet, surfaces are slate and navy, and the header is a navy band in both themes. In the light theme the chrome is grey and the content white, it used to be the other way round. There's also a slow light moving through the header. If that bothers you, &lt;code&gt;--prism-brand-motion: none&lt;/code&gt; stops it, and it holds still with reduced motion and in capture mode anyway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj8wvpxpnhsrgxt87kd4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj8wvpxpnhsrgxt87kd4.png" alt="the Button in the new look, dark and light side by side." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For plugin authors the building blocks of the built-in panels are exported from &lt;code&gt;@ng-prism/core&lt;/code&gt; now: &lt;code&gt;prism-badge&lt;/code&gt;, &lt;code&gt;prism-tabs&lt;/code&gt;, &lt;code&gt;prism-segmented&lt;/code&gt;, &lt;code&gt;prism-panel-toolbar&lt;/code&gt;, &lt;code&gt;prism-list-row&lt;/code&gt;, &lt;code&gt;prism-metric-card&lt;/code&gt; and the button directives. A panel built from them looks like the rest of the styleguide without copying any CSS. All scores also follow one rule now. a11y, coverage, VRT and perf use the same &lt;code&gt;statusFor()&lt;/code&gt;, so "danger" means below 75 % of the threshold everywhere, not only in the header.&lt;/p&gt;

&lt;p&gt;If you override theme tokens, check the deprecations below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle size that matches what you ship
&lt;/h2&gt;

&lt;p&gt;The perf plugin used to measure the raw &lt;code&gt;.ts&lt;/code&gt; file. That included the &lt;code&gt;@Showcase&lt;/code&gt; config, all the JSDoc and every comment, and &lt;code&gt;@ng-prism/core&lt;/code&gt; showed up as an import. So the better documented a component was, the worse it scored, which is the opposite of what I want.&lt;/p&gt;

&lt;p&gt;Now the file is measured without the decorator and without comments, stripped the same way your published build is. On a real library the largest component went from 77 kb (critical) to 35 kb (warning), and four components that only warned because of their docs are well below 20 kb now. It still measures TypeScript source and not compiled output, so it's an estimate, but at least it's an estimate of your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An active state shows up as a chip in the variant ribbon, and you can clear it from there.&lt;/li&gt;
&lt;li&gt;Every component opens on the Playground. The view doesn't carry over from the last component anymore.&lt;/li&gt;
&lt;li&gt;The sidebar scrolls the open component into view and expands its group after a reload or when you follow a link.&lt;/li&gt;
&lt;li&gt;The controls panel shows a Reset button as soon as an input differs from the variant.&lt;/li&gt;
&lt;li&gt;A directive output with a payload no longer breaks the build with TS2345.&lt;/li&gt;
&lt;li&gt;Stretch showcases fill the viewport width, and absolutely positioned elements stay inside their Overview cell.&lt;/li&gt;
&lt;li&gt;The resize grips are visible and work with touch.&lt;/li&gt;
&lt;li&gt;The docs start page has a one-minute tour.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deprecations before 23
&lt;/h2&gt;

&lt;p&gt;As usual, breaking changes within 22 come as deprecations first. These go away in 23.0.0, and the build tells you if you use them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The theme tokens &lt;code&gt;--prism-mark-nominal&lt;/code&gt;, &lt;code&gt;--prism-mark-attention&lt;/code&gt; and &lt;code&gt;--prism-mark-critical&lt;/code&gt;. Use &lt;code&gt;--prism-success&lt;/code&gt;, &lt;code&gt;--prism-warn&lt;/code&gt; and &lt;code&gt;--prism-danger&lt;/code&gt;. Your overrides still carry over until 23.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--prism-spectrum&lt;/code&gt;, &lt;code&gt;--prism-primary-from&lt;/code&gt; and &lt;code&gt;--prism-primary-to&lt;/code&gt;. Use &lt;code&gt;--prism-brand-gradient&lt;/code&gt;, or &lt;code&gt;--prism-primary-fill&lt;/code&gt; for fills. Overrides of these have no effect anymore.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;variant&lt;/code&gt; of a panel badge. Use &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;tone&lt;/code&gt; and &lt;code&gt;label&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A service or pipe with component fields like &lt;code&gt;variants&lt;/code&gt; or &lt;code&gt;host&lt;/code&gt; is still treated as a component until 23. Remove those fields, or move the demo to a component page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more thing if you read &lt;code&gt;__PRISM_MANIFEST__&lt;/code&gt; or use &lt;code&gt;onComponentScanned&lt;/code&gt; in a plugin: services, pipes and parts aren't in &lt;code&gt;components&lt;/code&gt; anymore. Services and pipes have their own lists and hooks, and parts sit under their parent's &lt;code&gt;parts&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng update @ng-prism/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the plugins you use at the same time, and the JSDoc plugin in particular, because the new API pages come from there: &lt;code&gt;ng update @ng-prism/core @ng-prism/plugin-jsdoc&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://ng-prism.github.io/ng-prism/demo/" rel="noopener noreferrer"&gt;ng-prism.github.io/ng-prism/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Services and pipes in the docs: &lt;a href="https://ng-prism.github.io/ng-prism/#/guide/showcasing-services" rel="noopener noreferrer"&gt;Showcasing services&lt;/a&gt;, &lt;a href="https://ng-prism.github.io/ng-prism/#/guide/showcasing-pipes" rel="noopener noreferrer"&gt;Showcasing pipes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Compiled templates: &lt;a href="https://ng-prism.github.io/ng-prism/#/guide/directive-hosting?id=host-template" rel="noopener noreferrer"&gt;Directive hosting&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/ng-prism/ng-prism/releases/tag/v22.7.0" rel="noopener noreferrer"&gt;github.com/ng-prism/ng-prism/releases/tag/v22.7.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/ng-prism/ng-prism" rel="noopener noreferrer"&gt;github.com/ng-prism/ng-prism&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which parts of your library still don't show up anywhere? Guards, interceptors, whole forms? I'd like to plan what comes next around real libraries, so let me know in the comments or open an issue.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hover isn't a variant: what's new in ng-prism 22.6</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 19 Oct 2026 21:31:25 +0000</pubDate>
      <link>https://dev.to/alexanderspies/hover-isnt-a-variant-whats-new-in-ng-prism-226-3fad</link>
      <guid>https://dev.to/alexanderspies/hover-isnt-a-variant-whats-new-in-ng-prism-226-3fad</guid>
      <description>&lt;h1&gt;
  
  
  Hover isn't a variant: what's new in ng-prism 22.6
&lt;/h1&gt;

&lt;p&gt;ng-prism 22.6 is out. If you haven't come across it yet: ng-prism is a component lab for Angular libraries, a Storybook alternative without story files. You put a &lt;code&gt;@Showcase&lt;/code&gt; decorator on a component, the TypeScript compiler picks up inputs, outputs and types at build time, and the components render in the same document as the styleguide, without an iframe. &lt;a href="https://dev.to/alexanderspies/what-an-angular-native-storybook-alternative-actually-looks-like-3jm3"&gt;The full tour&lt;/a&gt; has the details.&lt;/p&gt;

&lt;p&gt;The biggest change in this release is a new concept next to variants, called states. Besides that there's a Claude Code plugin for projects that use ng-prism, watch rebuilds that went from about 450 ms to about 60 ms, a few bug fixes and some deprecations ahead of version 23.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hover as a variant didn't work out
&lt;/h2&gt;

&lt;p&gt;In 22.5 I added &lt;code&gt;pseudo&lt;/code&gt; to variants. &lt;code&gt;pseudo: { hover: '.btn' }&lt;/code&gt; puts the button into &lt;code&gt;:hover&lt;/code&gt; while the variant is shown, using the component's own CSS. That worked, and pretty quickly the button showcase in the demo looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// …&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hovered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;pseudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pressed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;pseudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;pseudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus-visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a while this bugged me. These aren't really variants, they're the filled button again, just hovered. In the Playground they show up in the variant ribbon, and if you pick one, hover stays on while you play with the controls. They also only cover the combinations somebody wrote by hand. The filled button had a hover variant, the outlined one didn't. And hiding them from the ribbon wasn't an option either, because then they'd also disappear from the Overview and from the visual regression run, which is where they're actually useful.&lt;/p&gt;

&lt;p&gt;Hover, pressed and focus describe what is happening to a component, not what kind of component it is. So in 22.6 they get their own axis.&lt;/p&gt;

&lt;h2&gt;
  
  
  States
&lt;/h2&gt;

&lt;p&gt;A component now declares its states once, and every variant gets combined with them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ButtonComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pseudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pressed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pseudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pseudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus-visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// … Elevated, Text, Icon Only, Disabled, Readonly, On dark&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Full width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Full width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;canvasLayout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stretch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A state has a name and the same &lt;code&gt;pseudo&lt;/code&gt; object you already know from variants. That's &lt;code&gt;true&lt;/code&gt; for the whole component, or a selector (or a list of selectors) for the element the state belongs to, for example the inner &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; of a host component. The styling comes from your own CSS. ng-prism rewrites every rule that uses the pseudo-class, so &lt;code&gt;:hover&lt;/code&gt; becomes &lt;code&gt;:is(:hover, .prism-pseudo--hover)&lt;/code&gt;, and sets that class on the targets. You don't write any extra styles.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;states: false&lt;/code&gt; a variant opts out. A full-width button in hover state wouldn't show anything new.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Variant.pseudo&lt;/code&gt; is still there and isn't deprecated. For a single special case, like a text field with specific inputs plus focus, it's still the simpler choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Overview becomes a matrix
&lt;/h2&gt;

&lt;p&gt;Until now the Overview tab showed all variants side by side. If a component declares states, it shows every variant in every state:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fys2xn6hlc8uonedlw3uh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fys2xn6hlc8uonedlw3uh.png" alt="Overview of the Button: a matrix with one row per variant and the columns Default, Hover, Pressed and Focus visible. The Disabled row is filled only in Default, the other three cells are dashed and name the reason." width="799" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Button with nine variants, Default plus three states. Each column header shows the selector of the state and how many variants it fits.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each row is a variant, and its background and layout apply to the whole row. That's why &lt;code&gt;On dark&lt;/code&gt; is dark in every column. If you click a cell, or focus it and press Enter, the Playground opens with exactly that variant and state. This works for every component now, with or without states.&lt;/p&gt;

&lt;p&gt;Components without states keep the old Overview. And since the tab now counts cells instead of variants, a component with one variant and one state gets an Overview as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  A state only shows up where the browser could produce it
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;Disabled&lt;/code&gt; row three cells are empty. Hover and Pressed say &lt;code&gt;.btn has pointer-events: none&lt;/code&gt;, Focus visible says &lt;code&gt;.btn is disabled&lt;/code&gt;. That's on purpose.&lt;/p&gt;

&lt;p&gt;Technically ng-prism just sets a class. If your stylesheet draws a focus ring for that class, you'd get a focus ring on a disabled button, a state no user will ever see. For visual regression that's a real problem. The screenshot gets taken, becomes a baseline and is protected from then on, even though it shows something impossible.&lt;/p&gt;

&lt;p&gt;So a state only gets applied where the browser could produce it on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;focus&lt;/code&gt; and &lt;code&gt;focus-visible&lt;/code&gt; skip elements that are &lt;code&gt;:disabled&lt;/code&gt; (also inside a &lt;code&gt;&amp;lt;fieldset disabled&amp;gt;&lt;/code&gt;) or below &lt;code&gt;[inert]&lt;/code&gt;. &lt;code&gt;aria-disabled="true"&lt;/code&gt; doesn't count, because the browser can still focus that element. If the target is the whole component (&lt;code&gt;true&lt;/code&gt;), the focusable elements inside it decide.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hover&lt;/code&gt; and &lt;code&gt;active&lt;/code&gt; need an element that actually receives the pointer, so the target or one of its children must not have &lt;code&gt;pointer-events: none&lt;/code&gt;. A disabled button without &lt;code&gt;pointer-events: none&lt;/code&gt; does get hovered. If that cell then looks exactly like Default, good, because that's the test you want: the hover style doesn't leak into the disabled button.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;focus-within&lt;/code&gt; isn't filtered. Without a browser API for focusability there's no reliable way to tell whether some child could take focus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The selector counts too. The Chip's &lt;code&gt;Remove hovered&lt;/code&gt; state targets &lt;code&gt;.sg-chip__remove&lt;/code&gt;, and only removable chips render that button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Remove hovered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pseudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.sg-chip__remove&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqaxr3v63tvu8rvbzgznh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqaxr3v63tvu8rvbzgznh.png" alt="Overview of the Chip: two columns, Default and Remove hovered. The header reads " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;There's nothing to configure for this. Chips without a remove button get an empty cell with the reason.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are six possible reasons: &lt;code&gt;&amp;lt;target&amp;gt; matches nothing&lt;/code&gt;, &lt;code&gt;&amp;lt;target&amp;gt; is disabled&lt;/code&gt;, &lt;code&gt;&amp;lt;target&amp;gt; is inert&lt;/code&gt;, &lt;code&gt;&amp;lt;target&amp;gt; has pointer-events: none&lt;/code&gt;, &lt;code&gt;forces no state&lt;/code&gt; and &lt;code&gt;states: false&lt;/code&gt;. If a state doesn't fit a single variant, it's almost always a typo in the selector. In that case the column header gets marked and you get one warning in the console.&lt;/p&gt;

&lt;p&gt;One case took me longer than the rest. With hover-reveal CSS like &lt;code&gt;.row:not(:hover) .actions { pointer-events: none }&lt;/code&gt;, whether &lt;code&gt;.actions&lt;/code&gt; can be hovered depends on another forced class. An early version gave different results depending on how often the component had rendered. Now every pass measures the same way. It sets all forced classes, checks which targets are reachable and removes the class from the ones that aren't. So the first render, the one screenshot runners wait for, gives the same result as every later one.&lt;/p&gt;

&lt;h2&gt;
  
  
  States in the Playground
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96e3kk4ui4spqbr0sujf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96e3kk4ui4spqbr0sujf.png" alt="Playground with the Outlined button hovered. The tools menu is open and its State group shows the chips Hover (active), Pressed and Focus visible above the single pseudo-class toggles." width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The variant ribbon only shows variants again. The State group in the tools menu now has a button for each declared state, above the &lt;code&gt;:hover&lt;/code&gt; to &lt;code&gt;:focus-within&lt;/code&gt; toggles that were already there. Pick &lt;code&gt;Hover&lt;/code&gt; and it gets applied to whatever variant you're looking at.&lt;/p&gt;

&lt;p&gt;The state stays on when you switch to another variant, so you can click through a whole column of the matrix. When you switch to another component, it's reset. The controls keep working, so you can change the label of a hovered button and it stays hovered. The state is also part of the URL. &lt;code&gt;?component=ButtonComponent&amp;amp;variant=1&amp;amp;state=0&lt;/code&gt; opens the outlined button in hover state, &lt;a href="https://ng-prism.github.io/ng-prism/demo/?component=ButtonComponent&amp;amp;variant=1&amp;amp;state=0" rel="noopener noreferrer"&gt;here it is in the demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If a state doesn't fit the current variant, its button is dashed and the tooltip tells you why. You can still select it. It stays selected and kicks in as soon as the target exists:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hx3l5078pg56sjmqagx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hx3l5078pg56sjmqagx.png" alt="Chip " width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wrdkzwk279ba5gnajyw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wrdkzwk279ba5gnajyw.png" alt="The same Chip after switching on " width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;On the &lt;code&gt;Default&lt;/code&gt; chip there's nothing to hover. Turn on &lt;code&gt;removable&lt;/code&gt; in the controls and the state applies.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual regression
&lt;/h2&gt;

&lt;p&gt;ng-prism doesn't come with its own screenshot runner, because baselines, browser version and fonts belong to your CI. What it provides is a contract for driving the app, and 22.6 extends it. All of it is additive. Without states nothing changes, so existing runners and baselines keep working.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;window.__PRISM_MANIFEST__&lt;/code&gt; lists the states of each component as &lt;code&gt;states: [{ name, index, pseudo }]&lt;/code&gt;. A variant that opts out has &lt;code&gt;states: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;?state=&amp;lt;index&amp;gt;&lt;/code&gt; selects a state, next to &lt;code&gt;?variant=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;With a state, &lt;code&gt;data-prism-rendered&lt;/code&gt; is &lt;code&gt;"&amp;lt;ClassName&amp;gt;:&amp;lt;variant&amp;gt;:&amp;lt;state&amp;gt;"&lt;/code&gt;. Without one it stays &lt;code&gt;"&amp;lt;ClassName&amp;gt;:&amp;lt;variant&amp;gt;"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the state doesn't fit, &lt;code&gt;.demo-wrap&lt;/code&gt; gets &lt;code&gt;data-prism-excluded="&amp;lt;reason&amp;gt;"&lt;/code&gt;. The runner should report the cell as excluded instead of taking a screenshot.&lt;/li&gt;
&lt;li&gt;Capture mode (&lt;code&gt;?capture=1&lt;/code&gt;) applies the state and leaves some space around the component, so focus rings outside its box don't get cut off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the loop from the docs, a bit shortened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The variant on its own (null), then in each state, unless it opted out.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]))];&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;variant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;capture&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`.demo-wrap[data-prism-rendered="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;excluded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.demo-wrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-prism-excluded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;excluded&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;reportExcluded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;excluded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// status: 'excluded', with the reason&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.demo-wrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="c1"&gt;// compare against e.g. 01-outlined--hover.png&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index of a state is its position in the source. If the scanner can't read an entry, the slot stays there as an empty state. A typo in one state can't move the next one onto the wrong baseline that way.&lt;/p&gt;

&lt;p&gt;The VRT plugin reads two new optional fields from the report, &lt;code&gt;stateIndex&lt;/code&gt; and &lt;code&gt;stateName&lt;/code&gt;, and lists state captures next to their variant:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpefbga0whkx9ldsk9bk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpefbga0whkx9ldsk9bk6.png" alt="Visual Regression panel on the Button: the review list holds " width="799" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Claude Code plugin for your library
&lt;/h2&gt;

&lt;p&gt;Most coding agents have never seen ng-prism. Ask one to add a showcase and it guesses, usually with Storybook patterns. That's why the ng-prism repo is now also a Claude Code plugin marketplace. You don't have to clone anything, Claude Code fetches the plugin from GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;ng-prism &lt;span class="nt"&gt;--marketplace&lt;/span&gt; ng-prism/ng-prism
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a session it's &lt;code&gt;/plugin marketplace add ng-prism/ng-prism&lt;/code&gt; followed by &lt;code&gt;/plugin install ng-prism@ng-prism&lt;/code&gt;. With &lt;code&gt;--scope project&lt;/code&gt; the plugin gets enabled for the repo in &lt;code&gt;.claude/settings.json&lt;/code&gt;. Your teammates still have to install it once themselves, but after that it loads automatically in that repo. Plugins from third-party marketplaces don't update on their own by default, so get new versions with &lt;code&gt;claude plugin update ng-prism@ng-prism&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ng-prism:showcase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writing or changing &lt;code&gt;@Showcase&lt;/code&gt; configs, fixing &lt;code&gt;⚠ ng-prism:&lt;/code&gt; build warnings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ng-prism:setup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ng add&lt;/code&gt;, &lt;code&gt;ng-prism.config.ts&lt;/code&gt;, the official plugins, theming, publishing, upgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ng-prism:write-plugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your own plugin: build-time hooks, panels, controls, header widgets, sidebar decorations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The showcase skill doesn't stop after writing the config. It starts the styleguide, checks the build output for warnings and takes a screenshot of every variant it changed, using the Playwright from your project. So the agent actually looks at the result before saying it's done. The skills check which version of &lt;code&gt;@ng-prism/core&lt;/code&gt; you have installed and go by its typings, and the plugin version follows the ng-prism releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Faster watch rebuilds
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ng run my-lib:prism&lt;/code&gt; scans the library again on every save. The scanner already passed the previous &lt;code&gt;ts.Program&lt;/code&gt; to &lt;code&gt;ts.createProgram&lt;/code&gt;, but TypeScript only reuses a &lt;code&gt;SourceFile&lt;/code&gt; if the compiler host returns the exact same object. The default host parses the file again on every call, so each rebuild parsed the whole program, Angular's typings included. Now there's a cache that keeps the parsed &lt;code&gt;SourceFile&lt;/code&gt; as long as the file's mtime and size don't change.&lt;/p&gt;

&lt;p&gt;I measured it on the demo library with 15 components, 100 rebuilds with one component change each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;22.5&lt;/th&gt;
&lt;th&gt;22.6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warm rebuild, mean&lt;/td&gt;
&lt;td&gt;452 ms&lt;/td&gt;
&lt;td&gt;64 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm rebuild, p95&lt;/td&gt;
&lt;td&gt;524 ms&lt;/td&gt;
&lt;td&gt;102 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory (RSS)&lt;/td&gt;
&lt;td&gt;~372 MB&lt;/td&gt;
&lt;td&gt;~271 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There's a smaller one too. Reading the &lt;code&gt;paths&lt;/code&gt; from your tsconfig no longer lists every file below it. In the demo workspace that went from 9.25 ms to 0.02 ms per rebuild, and in bigger workspaces the difference is bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Aliased inputs and outputs. A variant couldn't set an input like &lt;code&gt;size = input('md', { alias: 'buttonSize' })&lt;/code&gt; because Angular threw NG0303, and a directive host with an alias broke the styleguide build with NG8002. Variants and controls still use the member name, which is what &lt;code&gt;InputsOf&amp;lt;T&amp;gt;&lt;/code&gt; expects, and ng-prism binds the public name. Aliases from a constant work too.&lt;/li&gt;
&lt;li&gt;Named content slots. If a component had a named &lt;code&gt;&amp;lt;ng-content select&amp;gt;&lt;/code&gt; before the default slot, variant &lt;code&gt;content&lt;/code&gt; ended up in the wrong slots. Each key now goes into the slot its selector matches.&lt;/li&gt;
&lt;li&gt;Libraries with only a primary entry point. If you set up such a library with &lt;code&gt;ng add&lt;/code&gt;, the styleguide showed no components and no warning either. The primary entry point is now scanned together with the secondary ones.&lt;/li&gt;
&lt;li&gt;Navigation. Back and forward between two components now restore the variant from the URL instead of jumping to variant 0, and switching the component from the Overview resets variant and state there as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deprecations before 23
&lt;/h2&gt;

&lt;p&gt;ng-prism versions follow the Angular majors, so a breaking change within 22 comes as a deprecation first. A few config and plugin options have existed since the first release without ever being used. 22.6 marks them as &lt;code&gt;@deprecated&lt;/code&gt;, the build prints a note for each one you set, and they'll be removed in 23.0.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NgPrismConfig&lt;/code&gt;: &lt;code&gt;ui&lt;/code&gt;, &lt;code&gt;appComponent&lt;/code&gt;, &lt;code&gt;themeStylesheet&lt;/code&gt;, &lt;code&gt;headless&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NgPrismPlugin.wrapComponent&lt;/code&gt; and a panel's &lt;code&gt;position&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;perfPlugin()&lt;/code&gt;: &lt;code&gt;thresholds&lt;/code&gt;, &lt;code&gt;bundle.excludeImports&lt;/code&gt;, &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;memory&lt;/code&gt; and the &lt;code&gt;PERF_PLUGIN_CONFIG&lt;/code&gt; token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them ever did anything, so removing them from your config doesn't change any behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  What states can't do
&lt;/h2&gt;

&lt;p&gt;States are CSS pseudo-classes only: &lt;code&gt;hover&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;focus&lt;/code&gt;, &lt;code&gt;focus-visible&lt;/code&gt; and &lt;code&gt;focus-within&lt;/code&gt;. Anything controlled by an input, like selected, expanded, loading or invalid, is still a variant.&lt;/p&gt;

&lt;p&gt;The limits of &lt;code&gt;pseudo&lt;/code&gt; from 22.5 still apply. A state after a pseudo-element (&lt;code&gt;::part(label):hover&lt;/code&gt;) can't be forced, Shadow DOM isn't supported, and stylesheets from another origin can only be rewritten if they're served with CORS.&lt;/p&gt;

&lt;p&gt;Also keep in mind that the matrix grows fast. 15 variants with 5 states means 90 screenshots per component. &lt;code&gt;states: false&lt;/code&gt; and selectors that only match where they make sense help with that, but think about which states you really need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng update @ng-prism/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the plugins you use at the same time, e.g. &lt;code&gt;ng update @ng-prism/core @ng-prism/plugin-visual-regression&lt;/code&gt;. There's nothing to migrate, &lt;code&gt;Variant.pseudo&lt;/code&gt; works exactly like in 22.5. If you want to switch to states, remove the hover, pressed and focus variants and declare the states once instead.&lt;/p&gt;

&lt;p&gt;You can try all of this in the live demo, a small component library with all plugins enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Button matrix: &lt;a href="https://ng-prism.github.io/ng-prism/demo/?component=ButtonComponent&amp;amp;view=overview" rel="noopener noreferrer"&gt;ng-prism.github.io/ng-prism/demo/?component=ButtonComponent&amp;amp;view=overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;States in the docs: &lt;a href="https://ng-prism.github.io/ng-prism/#/guide/variants?id=states" rel="noopener noreferrer"&gt;ng-prism.github.io/ng-prism/#/guide/variants?id=states&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release notes: &lt;a href="https://github.com/ng-prism/ng-prism/releases/tag/v22.6.0" rel="noopener noreferrer"&gt;github.com/ng-prism/ng-prism/releases/tag/v22.6.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/ng-prism/ng-prism" rel="noopener noreferrer"&gt;github.com/ng-prism/ng-prism&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm curious which states your components need that a CSS pseudo-class can't cover. Drag-over, open or loading maybe? I'd rather plan the next step around real components than around my own guesses, so let me know in the comments or open an issue.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What an Angular-native Storybook alternative actually looks like</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Tue, 22 Sep 2026 11:22:54 +0000</pubDate>
      <link>https://dev.to/alexanderspies/what-an-angular-native-storybook-alternative-actually-looks-like-3jm3</link>
      <guid>https://dev.to/alexanderspies/what-an-angular-native-storybook-alternative-actually-looks-like-3jm3</guid>
      <description>&lt;p&gt;&lt;em&gt;ng-prism puts the showcase config on the component, extracts the rest with the TypeScript compiler, and renders into your document instead of an iframe. This is the whole tool, end to end, including the three things that shipped in v22.3: a visual-regression panel, an Overview contact sheet, and a UI rebuilt around a single density scalar.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt2s6kmra99lno5yexhn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt2s6kmra99lno5yexhn.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://dyingangel666.github.io/ng-prism/demo/" rel="noopener noreferrer"&gt;dyingangel666.github.io/ng-prism/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://dyingangel666.github.io/ng-prism/" rel="noopener noreferrer"&gt;dyingangel666.github.io/ng-prism&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;github.com/dyingangel666/ng-prism&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The default, and what it costs
&lt;/h2&gt;

&lt;p&gt;If you build an Angular component library, you probably run Storybook. Not because you compared options, because it is what exists. It is the thing with the addons, the CI recipes and the Stack Overflow answers, and choosing anything else means explaining yourself.&lt;/p&gt;

&lt;p&gt;The price is quiet and you pay it every day. A parallel file tree of &lt;code&gt;*.stories.ts&lt;/code&gt; that drifts from the components it describes. A second build toolchain living next to the Angular CLI, with its own config, its own cache and its own upgrade cadence. A preview iframe that isolates your component from the document, which is exactly where CDK overlays, &lt;code&gt;MatDialog&lt;/code&gt; and anything else that portals to &lt;code&gt;body&lt;/code&gt; want to be. And an abstraction, the component as a flat bag of args, that was designed for a framework generation where inputs were plain properties. I wrote about &lt;a href="https://dev.to/dyingangel666/why-i-think-component-driven-development-needs-a-rethink-in-the-signal-era-i91"&gt;why that last one matters in the signal era&lt;/a&gt; already.&lt;/p&gt;

&lt;p&gt;ng-prism is what I built instead, I wrote up &lt;a href="https://dev.to/dyingangel666/why-i-built-ng-prism-an-angular-native-storybook-alternative-with-zero-story-files-1ip7"&gt;the original motivation&lt;/a&gt; when it was still an experiment. It is not a port of Storybook's ideas to Angular; it starts from what Angular already gives you, a compiler that knows your types, a builder system that knows your workspace, and a component model built on signals, and asks how little tooling you can get away with on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;You annotate the component. That is the whole authoring step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Showcase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ButtonComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inputs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flexible button with five visual variants.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Outlined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Elevated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;elevated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;button [class]="variant()" [disabled]="disabled()"&amp;gt;{{ label() }}&amp;lt;/button&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ButtonComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ButtonVariantType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;clicked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generic parameter is optional and worth taking: &lt;code&gt;@Showcase&amp;lt;ButtonComponent&amp;gt;&lt;/code&gt; type-checks every variant's &lt;code&gt;inputs&lt;/code&gt; against the component's actual input signals, so a renamed input breaks the build instead of silently rendering a default.&lt;/p&gt;

&lt;p&gt;At build time a scanner walks your library's entry point with the TypeScript Compiler API and extracts what it needs: which exports carry &lt;code&gt;@Showcase&lt;/code&gt;, what their inputs and outputs are called, what types they have, what the defaults are, what the JSDoc says. No runtime reflection, no &lt;code&gt;emitDecoratorMetadata&lt;/code&gt;, no decorator evaluation. The decorator itself is a no-op marker, and a shipped transformer (&lt;code&gt;ng-prism-strip&lt;/code&gt;) removes it from your compiled output, so publishing a library that uses ng-prism does not make your consumers install ng-prism.&lt;/p&gt;

&lt;p&gt;Setup is one command, and it wires ng-prism's builders into your existing &lt;code&gt;angular.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng add @ng-prism/core
ng run my-lib:prism        &lt;span class="c"&gt;# dev server on :4400&lt;/span&gt;
ng run my-lib:prism-build  &lt;span class="c"&gt;# static styleguide for CI / Pages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6d3h24v82ndl3nt0pd0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6d3h24v82ndl3nt0pd0v.png" alt="The ng-prism styleguide: sidebar with categories and status marks, component head, canvas with a rendered card, and the addon dock with plugin tabs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The whole surface: navigation on the left with per-category roll-ups, the component head on top, the stage in the middle with a floating tool rail, and the addon dock below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is the Playground. Controls are generated from the input signals the scanner found and pushed through &lt;code&gt;ComponentRef.setInput()&lt;/code&gt;, never property assignment, which on a signal input would overwrite the &lt;code&gt;InputSignal&lt;/code&gt; with a raw value and break the component on the next render. Events are logged in the Events tab. The code snippet in the tool rail updates as you change inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three consequences of being Angular-native
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No iframe.&lt;/strong&gt; Components are instantiated through &lt;code&gt;ViewContainerRef.createComponent()&lt;/code&gt; into the same document as the styleguide. Overlays land where they belong, focus trapping behaves, and &lt;code&gt;cdkConnectedOverlay&lt;/code&gt; positions against the real viewport. This is the single thing I would not give up, and the a11y panel below is the clearest argument for why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The build is your build.&lt;/strong&gt; &lt;code&gt;@ng-prism/core:serve&lt;/code&gt; and &lt;code&gt;@ng-prism/core:build&lt;/code&gt; are Angular builders. They read your workspace, your &lt;code&gt;tsconfig&lt;/code&gt;, your styles, your &lt;code&gt;appProviders&lt;/code&gt; (&lt;code&gt;provideHttpClient()&lt;/code&gt;, &lt;code&gt;provideAnimationsAsync()&lt;/code&gt;, whatever your components need). There is no second bundler with a second opinion about your SCSS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The docs write themselves.&lt;/strong&gt; The JSDoc plugin turns the comments you already have into an API view, inputs with types and defaults, outputs, examples, &lt;code&gt;@since&lt;/code&gt; and &lt;code&gt;@see&lt;/code&gt; links, and it stays correct because it is derived from the source, not from a hand-written args table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7uyqxsgbfm5o8c6fxzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7uyqxsgbfm5o8c6fxzv.png" alt="API view generated from JSDoc: description, variants list, examples, and a typed inputs table" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility is in the core, not in a plugin
&lt;/h2&gt;

&lt;p&gt;An audit you have to remember to install is an audit that does not run. So the a11y panel is not one of the six official plugins, it ships with &lt;code&gt;@ng-prism/core&lt;/code&gt;. The one thing you add is &lt;code&gt;axe-core&lt;/code&gt; itself, and even that is a dynamic import, so it is only pulled in when the panel actually audits something.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5vo3oc5sfnk97qsvai2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5vo3oc5sfnk97qsvai2.png" alt="A11y panel on the Toggle component: score ring at 75 out of 100, one critical button-name violation above the passing rules, tab-order overlay on the canvas" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A &lt;code&gt;critical&lt;/code&gt; finding on a toggle switch, next to the toggle, not in a CI log three hours later. The green &lt;code&gt;Tab 1&lt;/code&gt; chip on the canvas is the keyboard tab-order overlay.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The audit runs against the rendered variant and re-runs, debounced, whenever an input changes, so it describes the thing you are editing rather than a snapshot from before your last change. Four tabs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Violations&lt;/strong&gt; the live axe-core report, sorted by impact (critical → serious → moderate → minor), passing rules listed below the failures, and a score ring that moves as you fix things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard&lt;/strong&gt; the tab-order overlay visible in the screenshot, plus focus-trap detection and missing-&lt;code&gt;tabindex&lt;/code&gt; warnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARIA Tree&lt;/strong&gt; the accessible tree as the browser exposes it, with the computed accessible names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Reader&lt;/strong&gt; what a screen reader would announce, as a list or a step-through player, with a perspective toggle that dims the canvas so you stop seeing what your users do not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the no-iframe decision pays off most. Dialogs, tooltips, menus and anything else riding the CDK overlay are where accessibility violations actually hide, and here they are in the same document the audit runs against instead of portalled out of an isolated preview frame.&lt;/p&gt;

&lt;p&gt;When a rule is genuinely wrong for one component, you switch it off where the component is defined rather than in a global config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;a11y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when informing you is not enough: the same audit runs headless. Drive the built styleguide variant by variant, run axe-core against &lt;code&gt;.demo-wrap&lt;/code&gt;, write an &lt;code&gt;a11y-report.json&lt;/code&gt;, and set thresholds in &lt;code&gt;ng-prism.config.ts&lt;/code&gt; that fail the build, the header pill then carries the library-wide score into every PR preview. That is &lt;a href="https://dev.to/dyingangel666/a-year-of-the-eaa-does-your-component-librarys-ci-catch-a11y-violations-yet-34kb"&gt;its own post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Storybook still does better
&lt;/h2&gt;

&lt;p&gt;I would rather say this than have you find it out on day three.&lt;/p&gt;

&lt;p&gt;Storybook has an ecosystem I cannot match: interaction tests with &lt;code&gt;play()&lt;/code&gt; functions, Chromatic as a hosted visual-review service, MDX documentation, a decade of addons, and CI recipes for every provider. If your organisation ships React and Angular side by side, one tool across both is a real argument. If your workflow depends on component-level interaction testing inside the lab, Storybook has it and ng-prism does not, it gives you an Events panel and an external-tooling contract, which is a different and smaller thing.&lt;/p&gt;

&lt;p&gt;ng-prism is smaller, more opinionated, and maintained by one person. It buys you: no story files, no second toolchain, no iframe, signal-native input handling, and plugin panels that read build-time reports. Pick accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  New in v22.2, part I, the chrome went on a diet
&lt;/h2&gt;

&lt;p&gt;The old header stack was three bands of ornament above the thing you actually came to look at: an app header, a component header with a breadcrumb and a title and a status chip and a description and tags, a row of four stat tiles, then a view tab bar, then the variant ribbon. On a laptop, the component had the bottom third of the screen.&lt;/p&gt;

&lt;p&gt;v22.2 collapses that. The app header is 40px. The component head is one 36px row: breadcrumb, title, status, and then two buttons. Identity, selector, description, tags, variant count, lives behind an info popover. Measurement, coverage, a11y score, VRT diff, lives behind a gauge popover, with the head itself showing only the worst-deviating metric as a coloured chip.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpl8bebvrzfeomv7y9bwa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpl8bebvrzfeomv7y9bwa.png" alt="The component head with the measurement popover open, showing coverage, a11y score and VRT diff" width="800" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One 36px row. The chip names the metric that deviates most; the popover has the rest.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both popovers are the native Popover API, which means Escape, light dismiss and top-layer placement are browser behaviour rather than code I have to keep correct.&lt;/p&gt;

&lt;p&gt;The band heights are not hard-coded pixels. Each is &lt;code&gt;calc(&amp;lt;px&amp;gt; * var(--density))&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;--band-header&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;calc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;40&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--density&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c"&gt;/* app header    */&lt;/span&gt;
&lt;span class="nt"&gt;--band-head&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nt"&gt;calc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;36&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--density&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c"&gt;/* component head */&lt;/span&gt;
&lt;span class="nt"&gt;--band-rail&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nt"&gt;calc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;28&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--density&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c"&gt;/* variant ribbon */&lt;/span&gt;
&lt;span class="nt"&gt;--band-tabs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nt"&gt;calc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;34&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--density&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c"&gt;/* addon tab bar  */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the entire chrome rescales from one custom property. There is no density setting in a menu, because there does not need to be one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--density&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canvas tools moved off the band they used to occupy and onto a floating rail over the stage, guides, rulers, background and zoom, and the live Angular template for the current inputs. The sidebar gained per-component status marks with a per-category roll-up (&lt;code&gt;2/3&lt;/code&gt; next to a category means two of its three components carry a mark), and plugins can contribute their own marks through a public extension point.&lt;/p&gt;

&lt;h2&gt;
  
  
  New in v22.2, part II, Overview, a contact sheet
&lt;/h2&gt;

&lt;p&gt;The Playground is a viewfinder: one variant, big, with controls. That is the right tool for working on a variant and the wrong tool for the question "did the whole component survive the change I just made to the shared token file?", which needs all of them, at once, side by side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi1rilwcvajwwffuogyzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi1rilwcvajwwffuogyzv.png" alt="Overview tab: all nine Button variants as a grid of cells, each on its own declared background, with the full-width variant spanning the row" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every cell renders a real instance of the variant on the background that variant declared, &lt;code&gt;bg: 'dark'&lt;/code&gt; gets dark, an undeclared background gets the transparency checkerboard rather than a surface someone invented for it. A variant that stretches has no intrinsic width to compare against its neighbours, so it takes the whole row instead of a column.&lt;/p&gt;

&lt;p&gt;There are deliberately no controls. Columns follow the window; everything else a cell shows is what the variant itself declared. The tab appears only when a component has at least two variants, a contact sheet of one is just a worse Playground, and never for a component that renders through a registered page, because those read their state from a single global renderer service and &lt;em&gt;n&lt;/em&gt; cells would all show the same thing. That is a correctness argument, not a taste one.&lt;/p&gt;

&lt;p&gt;The cells also share none of the Playground's bookkeeping: no output subscriptions, no &lt;code&gt;prism:render:*&lt;/code&gt; performance marks, and crucially no &lt;code&gt;data-prism-rendered&lt;/code&gt; marker, the selector external screenshot runners key on. One rendered thing means one marker.&lt;/p&gt;

&lt;h2&gt;
  
  
  New in v22.2, part III, visual regression, next to the component
&lt;/h2&gt;

&lt;p&gt;Which brings us to the runner.&lt;/p&gt;

&lt;p&gt;ng-prism does &lt;strong&gt;not&lt;/strong&gt; ship a screenshot runner, and that is a decision rather than a gap. Capturing and comparing belongs to your CI container, because the container owns the three things that decide whether a diff is real: the baselines, the browser version, and the font stack. What ng-prism owns is the contract to drive the app and a rendering mode that makes the output deterministic.&lt;/p&gt;

&lt;p&gt;That contract is small. Every variant is addressable by URL (&lt;code&gt;?component=ButtonComponent&amp;amp;variant=1&lt;/code&gt;). The element to screenshot is &lt;code&gt;.demo-wrap&lt;/code&gt;, and it is public API, not an implementation detail. And &lt;code&gt;?capture=1&lt;/code&gt; puts the app into capture isolation mode: the canvas gets the whole viewport, zoom locks to 1, transitions and animations freeze document-wide, persisted session state is ignored, and every shell region that is not the canvas is hidden, by a structural rule rather than a selector list, so chrome changes like the tool rail moving do not invalidate a single baseline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?component=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;variant=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;capture=1`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* … data-prism-rendered … */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fonts, not the marker, are the flake&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.demo-wrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your runner compares those against baselines and writes a JSON report. &lt;code&gt;@ng-prism/plugin-visual-regression&lt;/code&gt; reads that report at build time and renders it where the reviewer already is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"auditedVariants"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unchanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"byVariant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"className"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ButtonComponent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"variantName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Outlined"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"variantIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"changed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// unchanged | changed | size-mismatch | new | excluded&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"diffRatio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.102624&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"baselinePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vrt/baseline/ButtonComponent/01-outlined.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"currentPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vrt/current/ButtonComponent/01-outlined.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"diffPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vrt/diff/ButtonComponent/01-outlined.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"baselineBg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"light"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3gsr0dflhyfx2v54e51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3gsr0dflhyfx2v54e51.png" alt="Visual Regression panel: grouped variant list with one changed and one new variant, and a wipe comparison between baseline and current" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few decisions in that panel I would defend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The list is grouped, not flat.&lt;/strong&gt; A library's variants are overwhelmingly unchanged, and fifteen identical cards bury the one you opened the panel for. &lt;code&gt;Needs review&lt;/code&gt; is always open and ordered worst-news-first; &lt;code&gt;Unchanged&lt;/code&gt; and &lt;code&gt;Excluded&lt;/code&gt; start collapsed. When nothing needs review the rule inverts and the first group opens, two collapsed headers and no rows reads as a panel that failed to load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;new&lt;/code&gt; is neutral.&lt;/strong&gt; A variant with no baseline has nothing to regress against. It sits in &lt;code&gt;Needs review&lt;/code&gt; because only a human can accept a baseline, but it is never painted as a failure, and the tab badge stays amber rather than red.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A diff is never drawn below 1:1.&lt;/strong&gt; Zoom offers &lt;code&gt;Fit&lt;/code&gt;, &lt;code&gt;1×&lt;/code&gt;, &lt;code&gt;2×&lt;/code&gt;, &lt;code&gt;4×&lt;/code&gt;, and a downscaled diff mask is one you cannot trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backgrounds are part of the comparison.&lt;/strong&gt; The report carries both the run's background and the baseline's. When they differ, the panel says so, instead of leaving a reviewer to hunt a regression in an untouched component that simply got photographed on a different surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The library-wide score becomes a coloured pill in the header, thresholded in your config, so a deployed PR preview shows its own VRT state before anyone clicks into a component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;visualRegressionPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reportPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vrt-report.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;assetBaseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The rest of the plugin fan
&lt;/h2&gt;

&lt;p&gt;Six official plugins, all registered the same way and all activated in both the build pipeline and the runtime:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;What it adds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSDoc&lt;/td&gt;
&lt;td&gt;The API view, inputs, outputs, examples, from your comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Figma&lt;/td&gt;
&lt;td&gt;Embedded design frame plus an in-browser design diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perf&lt;/td&gt;
&lt;td&gt;Render profiling per variant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Box Model&lt;/td&gt;
&lt;td&gt;Hover any element to inspect margin / border / padding / content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;Per-component test coverage read from an Istanbul or v8 summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VRT&lt;/td&gt;
&lt;td&gt;The report panel above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jvhkp9l3sgl0fg9gaac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jvhkp9l3sgl0fg9gaac.png" alt="Box Model plugin: hovering the rendered card highlights it and renders the nested margin, border, padding and content boxes" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Accessibility is deliberately absent from that list, for the reason given above: it is in core, not something you have to remember to install.&lt;/p&gt;

&lt;p&gt;Writing your own plugin is the same shape as the official ones: contribute panels, register custom controls for your own types, wrap every rendered component, or enrich the manifest at build time through &lt;code&gt;onComponentScanned&lt;/code&gt; / &lt;code&gt;onManifestReady&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng add @ng-prism/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;@Showcase&lt;/code&gt; to one component, run &lt;code&gt;ng run my-lib:prism&lt;/code&gt;, and you have a styleguide. Everything in this post is in the &lt;a href="https://dyingangel666.github.io/ng-prism/demo/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;, a small component library with all six plugins enabled, real coverage numbers and a real VRT report with a changed variant in it. Angular 20+ workspaces, tested against 20, 21 and 22; components need &lt;code&gt;input()&lt;/code&gt; / &lt;code&gt;output()&lt;/code&gt; signals.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://dyingangel666.github.io/ng-prism/demo/" rel="noopener noreferrer"&gt;dyingangel666.github.io/ng-prism/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://dyingangel666.github.io/ng-prism/" rel="noopener noreferrer"&gt;dyingangel666.github.io/ng-prism&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;github.com/dyingangel666/ng-prism&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it on a real library, the feedback I want most is where it breaks, a component that will not render, a control type that has no editor, a builder assumption that does not hold in your workspace. Issues and PRs welcome, and a star helps more than it should.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>angular</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A year of the EAA — does your component library's CI catch a11y violations yet?</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 22 Jun 2026 14:51:14 +0000</pubDate>
      <link>https://dev.to/alexanderspies/a-year-of-the-eaa-does-your-component-librarys-ci-catch-a11y-violations-yet-34kb</link>
      <guid>https://dev.to/alexanderspies/a-year-of-the-eaa-does-your-component-librarys-ci-catch-a11y-violations-yet-34kb</guid>
      <description>&lt;p&gt;&lt;em&gt;The EAA is one year into enforcement, and most component libraries still ship without a single automated a11y check. Here is how I closed that gap in an Angular workspace, on two levels — live during development, and as a hard fail in CI.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A year in, and we are still flying blind
&lt;/h2&gt;

&lt;p&gt;This week marks one year since the European Accessibility Act became enforceable on 28 June 2025. Member states started accepting complaints. Market surveillance authorities started asking questions. And a lot of teams that had been treating "we'll do an a11y pass before launch" as a strategy noticed, quietly, that there was no launch — there was a constant trickle of releases, and the a11y pass had never actually happened.&lt;/p&gt;

&lt;p&gt;Component libraries are where this hits first. They ship dozens or hundreds of building blocks, every one of them potentially the thing that makes your product fail an audit. Ask yourself, honestly: how many components in your design system pass &lt;code&gt;axe-core&lt;/code&gt; against WCAG 2.2 AA today? You don't know. Your CI doesn't know either.&lt;/p&gt;

&lt;p&gt;This post is about how to fix that for an Angular library — on two layers, both wired into the same tool, and both runnable in under an hour of setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-layer model
&lt;/h2&gt;

&lt;p&gt;There are two distinct moments where a11y feedback should land for a component library:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dev-time.&lt;/strong&gt; When I'm building a component variant, I want to see a11y violations &lt;em&gt;as they happen&lt;/em&gt;, against the exact variant I'm rendering, without leaving my workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI-time.&lt;/strong&gt; When a PR opens, I want every variant of every component re-audited and the build to fail loudly if the library regresses past a threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are not the same job. Dev-time is about catching cheaply. CI-time is about holding the line. You want both, and the tool I'll show — &lt;a href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;ng-prism&lt;/a&gt;, an Angular-native component showcase — wires both into the same workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Live a11y in the component lab
&lt;/h2&gt;

&lt;p&gt;ng-prism is a showcase tool similar in spirit to Storybook, but built around Angular's component model — I wrote about &lt;a href="https://dev.to/dyingangel666/why-i-built-ng-prism-an-angular-native-storybook-alternative-with-zero-story-files-1ip7"&gt;why I built it&lt;/a&gt; in an earlier post. You put &lt;code&gt;@Showcase&lt;/code&gt; on a component, ng-prism scans your library, and you get a styleguide app with controls, theming, panels — and an A11y panel built into the core.&lt;/p&gt;

&lt;p&gt;The panel runs &lt;code&gt;axe-core&lt;/code&gt; against the rendered variant every time the variant or any input changes. It has four tabs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Violations&lt;/strong&gt; — the live axe-core report, sorted by impact (critical, serious, moderate, minor), with a score ring that updates in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard&lt;/strong&gt; — tab-order overlay, focus trap detection, missing-&lt;code&gt;tabindex&lt;/code&gt; warnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARIA Tree&lt;/strong&gt; — the component's accessible tree as the browser sees it, with the computed accessible names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Reader&lt;/strong&gt; — what a screen reader would announce, as a list or step-through player with a "screen-reader perspective" toggle that dims the canvas so you stop seeing what users don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't add a plugin to get this. It comes with &lt;code&gt;@ng-prism/core&lt;/code&gt;. The only thing you ship into your workspace is &lt;code&gt;axe-core&lt;/code&gt; as a peer dependency.&lt;/p&gt;

&lt;p&gt;When a component needs a per-component override — a decorative icon that should not be audited, or a rule you know is wrong for this widget — you tag the showcase config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Toast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;a11y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;color-contrast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToastComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cheap layer. It catches the issues that would otherwise survive review because nobody pulled up DevTools and ran axe by hand. But the moment a contributor doesn't open the panel, the issue ships. That is what Layer 2 is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: a CI step that fails loud
&lt;/h2&gt;

&lt;p&gt;Here is the design call I want to spend a minute on: ng-prism does not ship a built-in audit CLI. I tried that, and pulled it out.&lt;/p&gt;

&lt;p&gt;Component libraries live in different stacks. Some teams already have Playwright. Some run &lt;code&gt;@axe-core/cli&lt;/code&gt; in a Docker step. Some have a custom headless harness for visual regression and want a11y bolted onto the same browser session. A built-in CLI would either lock teams into one of those, or balloon into a config surface that mirrors every option of all of them.&lt;/p&gt;

&lt;p&gt;So instead of a CLI, ng-prism exposes a small contract — call it the &lt;strong&gt;External Audit API&lt;/strong&gt; — and lets your team bring its own auditor. The contract has three anchors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anchor&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window.__PRISM_MANIFEST__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A global with &lt;code&gt;{ components: [{ className, variants: [{ name, index }] }], pages: [...] }&lt;/code&gt; set by the running app.&lt;/td&gt;
&lt;td&gt;Discovery. The auditor needs to know what to audit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?component=&amp;lt;className&amp;gt;&amp;amp;variant=&amp;lt;index&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL params the app reads on navigation.&lt;/td&gt;
&lt;td&gt;Drive the app to a specific variant from the outside.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[data-prism-rendered="&amp;lt;className&amp;gt;:&amp;lt;index&amp;gt;"]&lt;/code&gt; on &lt;code&gt;.demo-wrap&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;An attribute on the renderer host.&lt;/td&gt;
&lt;td&gt;Render-completion marker — wait for this to flip before injecting axe.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A working auditor against this contract is small. Here is the core loop in Playwright (adapted from the real script I run against my own library, ~250 lines total including a tiny static server and threshold checks):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForFunction&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__PRISM_MANIFEST__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__PRISM_MANIFEST__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;variant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.demo-wrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-prism-rendered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addScriptTag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;axeSource&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;violations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.demo-wrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;axe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;violations&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script writes an aggregated &lt;code&gt;a11y-report.json&lt;/code&gt; at the end. ng-prism reads that file at build time and embeds the totals into the runtime manifest, so a coloured pill (green/orange/red) appears in the styleguide header — visible to anyone reviewing a deployed PR preview.&lt;/p&gt;

&lt;p&gt;The thresholds live in your prism config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ng-prism.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;a11y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;reportPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a11y-report.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;serious&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;moderate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the report breaches any threshold, the &lt;strong&gt;build fails&lt;/strong&gt;. Not a warning, not a soft error — the pipeline goes red, the PR cannot merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it in CI
&lt;/h2&gt;

&lt;p&gt;The three steps for a GitHub Actions job (adapt to your runner of choice):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx nx run my-lib-prism:build&lt;/span&gt;       &lt;span class="c1"&gt;# 1. build the prism app&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx nx run my-lib:audit-a11y&lt;/span&gt;        &lt;span class="c1"&gt;# 2. your audit script writes a11y-report.json&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx nx run my-lib-prism:build&lt;/span&gt;       &lt;span class="c1"&gt;# 3. re-build to embed the report into the manifest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two builds, because the report is generated against the first build and embedded by the second. The header pill renders only after step 3 — useful if you deploy PR previews, because reviewers can see the a11y score next to the components without clicking through every panel.&lt;/p&gt;

&lt;p&gt;The audit step is your own script. If you don't want to write one, the Playwright snippet above plus argument parsing and threshold checks is roughly 200–250 lines and lives in &lt;code&gt;scripts/&lt;/code&gt;. It's yours, in your repo, in JS or TS you can edit when your CI changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this fits Angular libraries specifically
&lt;/h2&gt;

&lt;p&gt;A small note on tooling fit, because Angular libraries have one constraint that bites tooling all the time: dialogs and overlays. CDK Overlay, MatDialog, anything that portals to the body — they break in iframe-based component labs. ng-prism renders components directly into the host document via &lt;code&gt;ViewContainerRef.createComponent()&lt;/code&gt;, so overlays land where they should, and the a11y audit sees them as real DOM. That matters because most of the WCAG violations I catch are in dialogs and floating UI — exactly the surfaces other tools tend to miss.&lt;/p&gt;

&lt;p&gt;Beyond that: signal inputs and outputs work natively, the showcase config is just a decorator on the component (no separate story file), and a11y is in the core — not behind a plugin you have to remember to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng add @ng-prism/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add &lt;code&gt;@Showcase&lt;/code&gt; to a component, run the prism builder, open the A11y panel, and watch the score ring move when you change a variant. From there, the External Audit API documented in &lt;a href="https://github.com/dyingangel666/ng-prism/blob/main/docs/guide/accessibility.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/guide/accessibility.md&lt;/code&gt;&lt;/a&gt; is enough to write the CI script in an afternoon.&lt;/p&gt;

&lt;p&gt;The repo: &lt;a href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;github.com/dyingangel666/ng-prism&lt;/a&gt;. If this post saved your team a CI compliance scramble, a star is appreciated — and issues, PRs, and "this works / this doesn't" reports are very welcome.&lt;/p&gt;

&lt;p&gt;A year of the EAA is enough. Stop shipping component libraries that can't tell you their own a11y score.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>a11y</category>
      <category>typescript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why I think Component-Driven Development needs a rethink in the Signal era</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 18 May 2026 13:23:59 +0000</pubDate>
      <link>https://dev.to/alexanderspies/why-i-think-component-driven-development-needs-a-rethink-in-the-signal-era-i91</link>
      <guid>https://dev.to/alexanderspies/why-i-think-component-driven-development-needs-a-rethink-in-the-signal-era-i91</guid>
      <description>&lt;p&gt;&lt;em&gt;Component-Driven Development assumed a render model that signal-based Angular has quietly left behind. The tooling has not caught up, and I am not sure simply patching it will be enough.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment that made me write this
&lt;/h2&gt;

&lt;p&gt;I was building &lt;a href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;ng-prism&lt;/a&gt;, an Angular-native component showcase tool I maintain, and I needed to update a component's inputs from a controls panel. The naive version looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vcr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ButtonComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeDetectorRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works on a pre-signal component. On a signal-based component it quietly breaks the component. &lt;code&gt;variant&lt;/code&gt; and &lt;code&gt;label&lt;/code&gt; are no longer plain properties. They are &lt;code&gt;InputSignal&lt;/code&gt; objects, callable as &lt;code&gt;variant()&lt;/code&gt;. Angular stores the &lt;code&gt;InputSignal&lt;/code&gt; as a plain class field; there is no &lt;code&gt;defineProperty&lt;/code&gt; setter or proxy to intercept the write. So the assignment just overwrites the field reference with a string, and Angular never finds out. The next render then blows up the first time the template evaluates &lt;code&gt;variant()&lt;/code&gt;, because &lt;code&gt;variant&lt;/code&gt; is no longer a function. I verified this against &lt;code&gt;@angular/core&lt;/code&gt; 21.2.0: &lt;code&gt;createInputSignal()&lt;/code&gt; returns a plain function with a &lt;code&gt;[SIGNAL]&lt;/code&gt; symbol attached, no setter trap in sight.&lt;/p&gt;

&lt;p&gt;The fix is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;variant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the version that lives in ng-prism today, in the renderer effect that drives every showcase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/ng-prism/src/app/renderer/prism-renderer.component.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Simplified; real version also handles content projection and unknown-input warnings.&lt;/span&gt;
&lt;span class="nf"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rendererService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inputValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;componentRef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prism:rerender:start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// setInput() already marks dirty and schedules CD. The explicit&lt;/span&gt;
  &lt;span class="c1"&gt;// detectChanges() here forces it to run synchronously so the&lt;/span&gt;
  &lt;span class="c1"&gt;// performance.mark below wraps the actual render, not just the&lt;/span&gt;
  &lt;span class="c1"&gt;// dirty-marking. Also keeps timings predictable under zoneless.&lt;/span&gt;
  &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeDetectorRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prism:rerender:end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a tiny detail, but it is the symptom of a much bigger thing. The way Component-Driven Development (CDD) thinks about a component (props in, render out, args table on the side) was modelled on a world where component inputs were plain properties. That world is gone in Angular. And once you stop assuming it, a lot of the tooling we have built over the last decade starts to look like it is solving the wrong problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implicit model behind classic CDD
&lt;/h2&gt;

&lt;p&gt;Look at what almost every CDD tool, Storybook included, has agreed on for years:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A component is a pure-ish function of its inputs.&lt;/li&gt;
&lt;li&gt;Inputs are configured as a flat dictionary, the “args”.&lt;/li&gt;
&lt;li&gt;A “story” is a specific value of that dictionary.&lt;/li&gt;
&lt;li&gt;Changing args triggers a discrete re-render cycle.&lt;/li&gt;
&lt;li&gt;Addons (a11y, viewport, knobs, controls) hook into that cycle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the pre-Hooks era this was the right abstraction. React class components had &lt;code&gt;setState&lt;/code&gt;. Angular had &lt;code&gt;@Input()&lt;/code&gt; decorators and &lt;code&gt;ngOnChanges&lt;/code&gt;. Vue had options API. All of them had a clear, discrete moment where a property got assigned, the framework noticed, and the component re-rendered top-down. Args tables map onto that perfectly. Whatever knob you turn becomes a property assignment, which becomes an &lt;code&gt;ngOnChanges&lt;/code&gt; call, which becomes a render.&lt;/p&gt;

&lt;p&gt;This is also why Storybook’s args model felt so natural for so long. Args are properties. Properties are state. State drives render. Story = scenario = property snapshot.&lt;/p&gt;

&lt;p&gt;It was a beautiful, simple model. It is also, for Angular today, the wrong abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Signals actually changed
&lt;/h2&gt;

&lt;p&gt;I do not want to retread the “signals are great” territory. What matters for CDD is more specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inputs are no longer properties.&lt;/strong&gt; An &lt;code&gt;InputSignal&lt;/code&gt; is a callable getter. From outside the component, the only correct way to push a value into it is &lt;code&gt;ComponentRef.setInput(name, value)&lt;/code&gt;. There is no property to assign anymore. Tools that still build their abstractions on “assign this prop” are not even wrong yet, they just produce broken components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Components are nodes in a reactive graph, not pure functions of inputs.&lt;/strong&gt; Half of a real-world signal-based component is &lt;code&gt;computed()&lt;/code&gt; derived state. Those derived signals are part of the component’s behaviour. They are not inputs, but they are also not opaque internal state. An args table cannot represent them. A story shapes inputs, not derived state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle work is increasingly split between explicit hooks and reactive subscriptions.&lt;/strong&gt; &lt;code&gt;ngOnInit&lt;/code&gt; and &lt;code&gt;ngOnDestroy&lt;/code&gt; still exist and are still idiomatic for a lot of cases (subscriptions, teardown, one-shot setup). What has changed is that &lt;code&gt;ngOnChanges&lt;/code&gt; is largely irrelevant for signal inputs, and a growing share of the work that used to live in lifecycle hooks now lives inside &lt;code&gt;effect()&lt;/code&gt; or &lt;code&gt;computed()&lt;/code&gt; that is read by the template. The places where work happens have multiplied, and a chunk of it fires on a much finer-grained schedule than &lt;code&gt;mount → change → destroy&lt;/code&gt;. CDD tools that visualise lifecycle as those three states are visualising a real but shrinking slice of what the component actually does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-renders are continuous, not cycle-based.&lt;/strong&gt; Zoneless Angular still has &lt;code&gt;ApplicationRef.tick()&lt;/code&gt; and a &lt;code&gt;ChangeDetectionScheduler&lt;/code&gt;. What is gone is the &lt;em&gt;implicit&lt;/em&gt; tick driven by zone.js intercepting every async operation. In a zoneless app, change detection runs because the scheduler decided to run it, which in practice is because a signal flagged a view dirty. From the outside that looks less like a single render cycle and more like a graph of signals notifying their dependents at fine granularity. An addon that says “re-run my check on each render cycle” has no clean event to listen to, because there is no single render cycle visible at the public surface of the component.&lt;/p&gt;

&lt;p&gt;The pre-signal mental model is not catastrophically wrong. It is just lossy. And every layer of CDD tooling pays the price of that loss somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the tooling lags, concretely
&lt;/h2&gt;

&lt;p&gt;Three examples I have hit while building ng-prism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Controls and args still treat inputs as a flat property bag.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Storybook &lt;code&gt;args&lt;/code&gt; object and ng-prism’s own variant config both look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fine as a starting state. It is wrong as a model of how the component actually consumes its inputs. A signal-based input has a default expression, can be required, can be tied through &lt;code&gt;computed()&lt;/code&gt; into other state, and can be read multiple times per render. None of that is in the args object. The args object is a snapshot of a tree it cannot see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A11y, perf, and visual-diff addons assume “render happened, now check”.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;axe-core, layout measurements, screenshot capture: they all rely on a moment where the DOM has settled and you can inspect it. Angular does give you some primitives here. &lt;code&gt;afterNextRender()&lt;/code&gt; fires once after the next render. &lt;code&gt;afterRender()&lt;/code&gt; fires after every render. &lt;code&gt;ApplicationRef.isStable&lt;/code&gt; exposes a stable-state observable. None of those quite match what an a11y or visual-diff addon actually wants, which is closer to “the reactive graph has been quiet for N milliseconds across multiple microtasks, and I am now safe to walk the DOM”. That concept does not exist as a framework primitive. So you debounce, you wait for animation frames, you hope. In ng-prism the built-in a11y audit runs after a 500ms debounce on signal changes (&lt;code&gt;A11yAuditService.scheduleAudit&lt;/code&gt;, default &lt;code&gt;debounceMs = 500&lt;/code&gt;), which works in practice but feels like a workaround. I am not sure a true “graph is quiescent” signal even makes sense in a system designed to update continuously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The “scenario” unit is too coarse.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Storybook story is one set of args. A ng-prism variant is one set of input values. Neither can express “this component embedded in a parent that emits a signal stream over time”. The interesting bug in a signal-based component is rarely the static case. It is the transition. It is the moment when an upstream &lt;code&gt;computed()&lt;/code&gt; updates twice in the same microtask and your effect runs once instead of twice. You cannot represent that with &lt;code&gt;{ variant: 'primary' }&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried in ng-prism, honestly
&lt;/h2&gt;

&lt;p&gt;ng-prism started life with the same implicit model as Storybook. Components have inputs. Inputs have values. Variants are named tuples of input values. The decorator looks similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What ng-prism does that I think is on the right track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses &lt;code&gt;setInput()&lt;/code&gt; for every input push, never property assignment. So at least the signal contract is respected. This is the absolute floor and a surprising number of Angular-adjacent tools still get it wrong.&lt;/li&gt;
&lt;li&gt;Drives the rendering loop with &lt;code&gt;effect()&lt;/code&gt; on a &lt;code&gt;signal&lt;/code&gt; of input values, not with a render-cycle event. The renderer reacts to whatever upstream signal happens to change.&lt;/li&gt;
&lt;li&gt;Treats the scanner as a build-time concern. Signal inputs are recognised via the TypeScript Compiler API at build time, so the runtime never has to read decorator metadata. The decorator itself is literally a no-op, just a marker.&lt;/li&gt;
&lt;li&gt;Supports zoneless via an opt-in flag in the &lt;code&gt;ng add&lt;/code&gt; schematic (&lt;code&gt;--zoneless&lt;/code&gt;), which wires up &lt;code&gt;provideZonelessChangeDetection()&lt;/code&gt; and drops &lt;code&gt;zone.js&lt;/code&gt; from polyfills. The default still ships with zone.js, but nothing in the renderer depends on an implicit zone tick; change detection runs because something signal-shaped told it to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What ng-prism does &lt;strong&gt;not&lt;/strong&gt; solve yet, and where I think the whole category is still stuck:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The variant model is still a flat input snapshot. I cannot describe a component as “embedded in a parent that pushes this signal stream over 2 seconds”.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;computed()&lt;/code&gt; derived state is invisible in the UI. You see the inputs you set. You do not see the graph that hangs off them. For some components, that graph is the interesting part.&lt;/li&gt;
&lt;li&gt;The a11y, perf, and box-model panels all hook into renderer output the way an old addon would. They debounce. They do not subscribe to the actual reactive graph the component is part of.&lt;/li&gt;
&lt;li&gt;The “code snippet” feature generates a template string from input values. It cannot show how the component would behave in a parent where one of those inputs is itself a signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am being deliberate about this in the docs. I do not want ng-prism to claim a level of insight into signal-based components that it does not yet deliver.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think the next generation should look like
&lt;/h2&gt;

&lt;p&gt;This is the speculative part, so take it as opinion.&lt;/p&gt;

&lt;p&gt;I think the unit of CDD will stop being “the component with these props” and start being “the component embedded in a reactive context”. A scenario will look more like a tiny fixture: this component, mounted under this parent, with these signal sources feeding it, over time. Closer to a Playwright scenario than a Storybook story.&lt;/p&gt;

&lt;p&gt;I think tooling will need to render the reactive graph, not just the visual output. For a serious component, the dependency graph between inputs, &lt;code&gt;computed()&lt;/code&gt;, and &lt;code&gt;effect()&lt;/code&gt; is the documentation. We render the box and the controls. We should also be able to render the graph.&lt;/p&gt;

&lt;p&gt;I think the addon model needs to flip. Instead of hooking into a render lifecycle that no longer exists, addons should subscribe to specific signals. Visual diff subscribes to the renderedElement signal. A11y subscribes to the same. Perf subscribes to a “graph quiescent” signal that the framework would have to expose. None of this lives in Storybook’s current architecture, and bolting it on is not obviously the right answer.&lt;/p&gt;

&lt;p&gt;And I think “args” as a UI metaphor has run its course. A signal-based component is not configured by setting properties. It is wired up. The control surface should reflect that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I am not arguing that Storybook is bad, or that the people maintaining it have missed something obvious. They built the right tool for the framework world of 2018, and that tool became a standard. Standards lag by definition. That is fine.&lt;/p&gt;

&lt;p&gt;What I am arguing is that the abstraction is starting to leak in ways that matter. Signal-based Angular is a different kind of component runtime, not a faster version of the old one, and the surface area that CDD tooling was designed against has changed underneath it.&lt;/p&gt;

&lt;p&gt;I am building ng-prism partly to test that hypothesis in code, not just in prose. I am also fully prepared to find out that I am wrong, that an args table plus &lt;code&gt;setInput()&lt;/code&gt; plus a debounce is good enough for 95% of cases, and that the rest is academic. Possible. But I do not think it is, and I would rather find out by building than by predicting.&lt;/p&gt;

&lt;p&gt;If you have hit the same kind of friction, or you have a counter-example where the args model still maps cleanly onto a signal-heavy component, I would genuinely like to hear it. That is more useful than another “signals are great” thread.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;ng-prism is open source on &lt;a href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Feedback and counter-examples especially welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>storybook</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Built ng-prism — An Angular-Native Storybook Alternative With Zero Story Files</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sat, 02 May 2026 19:03:21 +0000</pubDate>
      <link>https://dev.to/alexanderspies/why-i-built-ng-prism-an-angular-native-storybook-alternative-with-zero-story-files-1ip7</link>
      <guid>https://dev.to/alexanderspies/why-i-built-ng-prism-an-angular-native-storybook-alternative-with-zero-story-files-1ip7</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbxo8mppys9eyqg24mr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbxo8mppys9eyqg24mr6.png" alt="Example Screenshot of NgPrism" width="800" height="640"&gt;&lt;/a&gt;           &lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Every Angular Team Knows
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;.stories.ts&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I wanted something different. Something that feels like Angular because &lt;em&gt;it is&lt;/em&gt; Angular.                                                                                                                       &lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing ng-prism
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;@Showcase&lt;/code&gt; decorator, and ng-prism discovers it at build time via the TypeScript Compiler API.&lt;/p&gt;

&lt;p&gt;No story files. No parallel file tree. The documentation lives where the code lives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Showcase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                                                                                                                                                   
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                             
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Atoms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                         
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The primary action button.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;                                                                                                                                                                                                  
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                                                                                                                                        
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ghost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cancel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ghost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                                                                                                                                         
    &lt;span class="p"&gt;],&lt;/span&gt;                                                                                                                                                                                                           
  &lt;span class="p"&gt;})&lt;/span&gt;                                                                                                                                                                                                           
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;                                                                                                                                                                                                   
    &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                    
    &lt;span class="na"&gt;standalone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                            
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;button [class]="variant()"&amp;gt;{{ label() }}&amp;lt;/button&amp;gt;`&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;                                                                                                                                                                                                             
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ButtonComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                                                                               
    &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                                                                                                                                                                            
    &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ghost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;clicked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                                                                                                                                                                                    
  &lt;span class="p"&gt;}&lt;/span&gt;                                                                                                                                                                                                            
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. Run &lt;code&gt;ng run my-lib:prism&lt;/code&gt;, 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.                                                                                                                                                                                                       &lt;/p&gt;
&lt;h2&gt;
  
  
  How It Works Under the Hood
&lt;/h2&gt;

&lt;p&gt;ng-prism doesn't guess your component's API. It reads it. At build time, a custom Angular Builder kicks off a pipeline:                                                                                        &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript Compiler API scanner&lt;/strong&gt; — Parses your library's entry point, walks the AST, and extracts every component annotated with &lt;code&gt;@Showcase&lt;/code&gt;. It reads &lt;code&gt;input()&lt;/code&gt; and &lt;code&gt;output()&lt;/code&gt; signal declarations,
infers types, detects defaults, and builds a complete component manifest.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin hooks&lt;/strong&gt; — Registered plugins can enrich the scanned data (e.g., extracting JSDoc comments, injecting Figma URLs from metadata).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime manifest generation&lt;/strong&gt; — The pipeline produces a TypeScript file with real import statements pointing to your actual component classes. No JSON serialization, no runtime reflection.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Angular Dev Server&lt;/strong&gt; — The builder delegates to &lt;code&gt;@angular-devkit/architect&lt;/code&gt;, so you get the Angular dev server you already know — HMR, source maps, the works.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: your styleguide is a regular Angular app. No iframe. Components render in the same document context. &lt;code&gt;MatDialog&lt;/code&gt;? Works. CDK Overlay? Works. CSS custom properties from a parent theme? Inherited naturally.                                                                                                                                                                                                     &lt;/p&gt;
&lt;h2&gt;
  
  
  Signal-Native From Day One
&lt;/h2&gt;

&lt;p&gt;ng-prism was built for Angular 21+ and the signal API. It understands &lt;code&gt;input()&lt;/code&gt;, &lt;code&gt;input.required()&lt;/code&gt;, and &lt;code&gt;output()&lt;/code&gt; natively. The controls panel automatically generates the right control widget for each&lt;br&gt;&lt;br&gt;
  input type:                                                                                                                                                                                                  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;string&lt;/code&gt; → text field
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;boolean&lt;/code&gt; → toggle&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;number&lt;/code&gt; → number input&lt;/li&gt;
&lt;li&gt;Union types like &lt;code&gt;'primary' | 'danger'&lt;/code&gt; → dropdown
&lt;/li&gt;
&lt;li&gt;Complex types → JSON editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you change a value in the controls panel, it flows through Angular's signal system. There's no &lt;code&gt;@Input()&lt;/code&gt; decorator support — and that's by design. If you're starting a new component library in 2026, you should be using signals.                                                                                                                                                                                   &lt;/p&gt;
&lt;h2&gt;
  
  
  Built-In Accessibility Auditing
&lt;/h2&gt;

&lt;p&gt;Accessibility isn't a plugin in ng-prism — it's a core feature. The built-in A11y panel provides four perspectives on every component:                                                                         &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Violations&lt;/strong&gt; — axe-core audit with a visual score ring, sorted by impact severity
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard Navigation&lt;/strong&gt; — Tab order visualization with overlay indicators
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARIA Tree&lt;/strong&gt; — The accessibility tree as the browser sees it
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Reader Simulation&lt;/strong&gt; — Step through your component the way a screen reader would, with play/pause navigation
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.                               &lt;/p&gt;
&lt;h2&gt;
  
  
  Plugin Architecture — Extend Everything
&lt;/h2&gt;

&lt;p&gt;ng-prism follows a Vite-style plugin model. A plugin is a plain object with optional hooks for both build time and runtime:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NgPrismPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/core/plugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                                                                                                                                  

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;myPlugin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;NgPrismPlugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                                                                                                     
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-plugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                       

      &lt;span class="c1"&gt;// Build-time: enrich scanned component data&lt;/span&gt;
      &lt;span class="nf"&gt;onComponentScanned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                                                                            
        &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;myData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;extractSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;                                                                                                                           
      &lt;span class="p"&gt;},&lt;/span&gt;                                                                                                                                                                                                         

      &lt;span class="c1"&gt;// Runtime: add a panel to the UI                                                                                                                                                                          &lt;/span&gt;
      &lt;span class="na"&gt;panels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;                                                                                                                                                                                               
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-panel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Panel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                       
        &lt;span class="na"&gt;loadComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./my-panel.component.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MyPanelComponent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bottom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                      
        &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                    
      &lt;span class="p"&gt;}],&lt;/span&gt;                                                                                                                                                                                                        
    &lt;span class="p"&gt;};&lt;/span&gt;                                                                                                                                                                                                         
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Official Plugins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;@ng-prism/plugin-jsdoc&lt;/strong&gt; — Extracts JSDoc comments at build time and generates structured API documentation, including parameter tables
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@ng-prism/plugin-figma&lt;/strong&gt; — Embeds Figma designs as interactive iframes to enable direct visual comparison with components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@ng-prism/plugin-box-model&lt;/strong&gt; — Overlays CSS box model dimensions directly on rendered components for layout inspection
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@ng-prism/plugin-perf&lt;/strong&gt; — Profiles initial render and re-render performance using the browser Performance API
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@ng-prism/plugin-coverage&lt;/strong&gt; — Displays per-component test coverage based on Istanbul/V8 reports
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugins lazy-load their components, so they don't bloat your initial bundle.                                                                                                                                   &lt;/p&gt;
&lt;h2&gt;
  
  
  Setup in Two Minutes
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  ng add @ng-prism/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The schematic asks which library you want to showcase, creates a prism app project, wires up the builder targets in &lt;code&gt;angular.json&lt;/code&gt;, and generates a config file. Then:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  ng run my-lib:prism                                                                                                                                                                                          
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Your styleguide is running at &lt;code&gt;localhost:4200&lt;/code&gt;. For the config, you get a typed &lt;code&gt;prism.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                                                                                                                                               
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jsDocPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/plugin-jsdoc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;figmaPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/plugin-figma&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;                                                                                                                                                                                                   
      &lt;span class="nf"&gt;jsDocPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;                                                                                                                                                                                           
      &lt;span class="nf"&gt;figmaPlugin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--prism-primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6366f1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--prism-primary-from&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6366f1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--prism-primary-to&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#8b5cf6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;                                                                                                                                                                            
      &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;                                                                                                                                                                                                            
  &lt;span class="p"&gt;});&lt;/span&gt;                                                                                                                                                                                                          
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Watch Mode
&lt;/h2&gt;

&lt;p&gt;The serve builder watches your library sources and config file. Change a component, add a &lt;code&gt;@Showcase&lt;/code&gt; decorator, modify an input — the manifest regenerates, and the Angular dev server picks up the change. No restart needed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Component Pages &amp;amp; Custom Pages
&lt;/h2&gt;

&lt;p&gt;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.                                             &lt;/p&gt;

&lt;p&gt;ng-prism supports &lt;strong&gt;Component Pages&lt;/strong&gt; — free-form Angular components registered alongside your showcased components. They're defined in your &lt;code&gt;main.ts&lt;/code&gt; via &lt;code&gt;providePrism()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismShellComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providePrism&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;componentPage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ng-prism/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                                                                                                           
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ButtonPatternsPageComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./pages/button-patterns.page.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                                                                                                                 

  &lt;span class="nf"&gt;bootstrapApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PrismShellComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                                                                    
    &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;                                                                                                                                                                                                 
      &lt;span class="nf"&gt;providePrism&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                                                                         
        &lt;span class="na"&gt;componentPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nf"&gt;componentPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Button Patterns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Atoms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                   
            &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ButtonPatternsPageComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;                                                                                                                                                                                                    
        &lt;span class="p"&gt;],&lt;/span&gt;                                                                                                                                                                                                     
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For static pages that don't need Angular components, use &lt;strong&gt;Custom Pages&lt;/strong&gt; directly in &lt;code&gt;prism.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;                                                                                                                                                                                
    &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;                                                                                                                                                                                                     
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Changelog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;                                                                                                                                                                                                           
  &lt;span class="p"&gt;});&lt;/span&gt;                                                                                                                                                                                                          
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both appear in the sidebar navigation alongside regular components.&lt;/p&gt;
&lt;h2&gt;
  
  
  Content Projection &amp;amp; Directive Hosting
&lt;/h2&gt;

&lt;p&gt;Real-world components use &lt;code&gt;&amp;lt;ng-content&amp;gt;&lt;/code&gt;. ng-prism handles this with a &lt;code&gt;content&lt;/code&gt; property on variants:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Showcase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;                                                                                                                                                                                                  
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                               
    &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;With Header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                       
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                                                                                               
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[card-header]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h3&amp;gt;Title&amp;lt;/h3&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Body content&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;                                                                                                                                                                                                          
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;                                                                                                                                                                                                             
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Need to showcase a directive instead of a component? Use the &lt;code&gt;host&lt;/code&gt; property to specify what element it attaches to — either a plain HTML string or another Angular component.                                 &lt;/p&gt;
&lt;h2&gt;
  
  
  Why Not Just Use Storybook?
&lt;/h2&gt;

&lt;p&gt;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:                                                                 &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintaining a parallel &lt;code&gt;.stories.ts&lt;/code&gt; file tree that drifts out of sync
&lt;/li&gt;
&lt;li&gt;Fighting iframe restrictions when your component uses overlays or portals
&lt;/li&gt;
&lt;li&gt;Configuring a separate build system (webpack/Vite) alongside the Angular CLI
&lt;/li&gt;
&lt;li&gt;Wrapping Angular-specific patterns (dependency injection, signals) in framework-agnostic abstractions
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…then ng-prism might be worth a look. It doesn't try to be framework-agnostic. It's Angular, all the way down.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Road Ahead
&lt;/h2&gt;

&lt;p&gt;ng-prism is open source under the MIT license and follows Angular's versioning: &lt;code&gt;@ng-prism/core@21.x&lt;/code&gt; 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.                                                                                                                                            &lt;/p&gt;

&lt;p&gt;What's coming next:                                                                                                                                                                                            &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More official plugins&lt;/li&gt;
&lt;li&gt;CI integration — Manifest validation in your pipeline (e.g. ensuring every public component has a @Showcase decorator)
&lt;/li&gt;
&lt;li&gt;Design token documentation — Automatic token overview extracted from SCSS / CSS custom properties
&lt;/li&gt;
&lt;li&gt;Export — Share your component catalog as a PDF or static HTML page
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://dyingangel666.github.io/ng-prism/demo/" rel="noopener noreferrer"&gt;Demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://dyingangel666.github.io/ng-prism/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;code&gt;npm install @ng-prism/core&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dyingangel666" rel="noopener noreferrer"&gt;
        dyingangel666
      &lt;/a&gt; / &lt;a href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;
        ng-prism
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Lightweight Angular-native component showcase — no story files needed.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/dyingangel666/ng-prism/docs/prism_cover.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdyingangel666%2Fng-prism%2FHEAD%2Fdocs%2Fprism_cover.png" alt="ng-prism: Angular components, refracted" width="100%"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;ng-prism&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A component showcase for modern Angular, without story files
You add one decorator to your component. ng-prism picks it up at build time and generates live controls, code snippets and docs from it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@ng-prism/core" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/378046145d958e926c4f9fc49eec8ef0f80a0a4094f6e7828f3ad7e9b8d19792/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f406e672d707269736d2f636f7265" alt="npm"&gt;&lt;/a&gt;
&lt;a href="https://www.npmjs.com/package/@ng-prism/core" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6b2f00906e39724d5b965c8b4cabb4adbc33b437f3b03cb3ffbef40264f5af77/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f406e672d707269736d2f636f7265" alt="downloads"&gt;&lt;/a&gt;
&lt;a href="https://angular.dev" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a5bd0e8d5a5ed9a07bef8bfe5122e61f914aaf3a91f18a1a193c116336ef8315/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f416e67756c61722d32302b2d646430303331" alt="Angular"&gt;&lt;/a&gt;
&lt;a href="https://www.typescriptlang.org" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e81b2c93b039aa4f8f88871be055cc5342af7f71ccbbdf6d68e3f19fea31a985/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d352e352b2d333137386336" alt="TypeScript"&gt;&lt;/a&gt;
&lt;a href="https://github.com/dyingangel666/ng-prism/./LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dyingangel666.github.io/ng-prism/demo/?component=ButtonComponent" rel="nofollow noopener noreferrer"&gt;Live demo&lt;/a&gt; · &lt;a href="https://dyingangel666.github.io/ng-prism/" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Same button, two approaches&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;th&gt;Storybook&lt;/th&gt;

&lt;th&gt;ng-prism&lt;/th&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;

&lt;div class="highlight highlight-source-ts notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;// button.stories.ts (separate file)&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-k"&gt;type&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;Meta&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-v"&gt;StoryObj&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;'@storybook/angular'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;ButtonComponent&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;'./button.component'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;meta&lt;/span&gt;: &lt;span class="pl-smi"&gt;Meta&lt;/span&gt;&lt;span class="pl-c1"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-smi"&gt;ButtonComponent&lt;/span&gt;&lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;title&lt;/span&gt;: &lt;span class="pl-s"&gt;'Atoms/Button'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;component&lt;/span&gt;: &lt;span class="pl-v"&gt;ButtonComponent&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;argTypes&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-c1"&gt;variant&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
            &lt;span class="pl-c1"&gt;control&lt;/span&gt;: &lt;span class="pl-s"&gt;'select'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;options&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;'primary'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'secondary'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'danger'&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;
        &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;export&lt;/span&gt; &lt;span class="pl-k"&gt;default&lt;/span&gt; &lt;span class="pl-s1"&gt;meta&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;type&lt;/span&gt; &lt;span class="pl-smi"&gt;Story&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;StoryObj&lt;/span&gt;&lt;span class="pl-c1"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-smi"&gt;ButtonComponent&lt;/span&gt;&lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;export&lt;/span&gt; &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-v"&gt;Primary&lt;/span&gt;: &lt;span class="pl-smi"&gt;Story&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;args&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;variant&lt;/span&gt;: &lt;span class="pl-s"&gt;'primary'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;label&lt;/span&gt;: &lt;span class="pl-s"&gt;'Click me'&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;export&lt;/span&gt; &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-v"&gt;Danger&lt;/span&gt;: &lt;span class="pl-smi"&gt;Story&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;args&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;variant&lt;/span&gt;: &lt;/pre&gt;…
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/dyingangel666/ng-prism" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                                                                                                                                                        &amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
