Guide 07
Playground Blocks
cngxjs (opens in new tab) Updated Guide 07 of 13
Components can ship author-curated, runnable demos that open in a fresh StackBlitz tab. Add a
@playground JSDoc block, and compodocx renders a dedicated Playground tab on the component page with one launch button per block. The StackBlitz project is assembled
at build time. The SDK is lazy-loaded on first click, so static doc pages stay light.
Three authoring modes
Pick the mode that matches the demo. All three share the same scaffold — only the AppComponent body differs.
- Inline snippet.
@playground <title>followed by a fenced HTML or TS code block in the JSDoc. Best for short single-template demos. - HTML file reference.
@playground <title> ./path/to/file.html— the file body becomes the AppComponent template literal. Best for longer markup demos. - TS component file reference.
@playground <title> ./path/to/file.component.ts— a real@Componentclass replaces the AppComponent. Best for stateful demos with signals, event handlers, or composed widgets.
Mode 1 — Inline snippet
The original authoring mode. The full demo body lives inside a fenced code block in the JSDoc comment.
/**
* Reusable button.
*
* @playground Default
* ```html
* <my-button label="Save changes"></my-button>
* ```
*
* @playground Disabled
* ```html
* <my-button label="Save changes" [disabled]="true"></my-button>
* ```
*/
@Component({ /* ... */ })
export class MyButton { /* ... */ }
Multiple @playground blocks render as a vertical stack of sections in
source order, each with its own “Open in StackBlitz” button. The tab is hidden automatically
when no block is parsed. Zero-config when you don’t need it.
Mode 2 — HTML file reference
Append a relative path ending in .html to the title line. The file’s
contents become the AppComponent template body. Path is resolved against the file holding the JSDoc
comment.
/**
* Reusable button.
*
* @playground Showcase ./examples/button-showcase.html
*/
@Component({ /* ... */ })
export class MyButton { /* ... */ } <section class="demo">
<my-button label="Solid">Solid</my-button>
<my-button label="Ghost" tone="ghost">Ghost</my-button>
<my-button label="Disabled" [disabled]="true">Disabled</my-button>
</section> Best for demos long enough that JSDoc gets visually noisy. Editor lints the HTML, syntax highlighting works as expected, diffs stay clean.
Mode 3 — TS component file reference
Append a relative path ending in .ts. The file must export a real
standalone @Component class — it REPLACES the AppComponent in
the StackBlitz project entirely. Optional templateUrl,
styleUrl, styleUrls siblings and relative
imports are walked and packed automatically.
/**
* Reusable button.
*
* @playground Counter ./examples/counter/counter-example.component.ts
*/
@Component({ /* ... */ })
export class MyButton { /* ... */ } import { Component, signal } from '@angular/core';
import { MyButton } from '../../button.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [MyButton],
templateUrl: './counter-example.component.html',
styleUrl: './counter-example.component.css'
})
export class CounterExample {
readonly count = signal(0);
increment(): void {
this.count.update(n => n + 1);
}
}
Use selector: ‘app-root’ on the entry — that is the
host the scaffold bootstraps. The class name itself can be anything; compodocx appends
export { YourClass as AppComponent } automatically so the
import in src/main.ts resolves.
Recommended folder layout for a library component with multiple demos:
src/lib/button/
├── button.component.ts ← @playground tags
├── button.component.html
├── button.component.css
└── examples/
├── default/
│ ├── default-example.component.ts (mode 3 entry)
│ ├── default-example.component.html (templateUrl sibling)
│ └── default-example.component.css (styleUrl sibling)
├── disabled/
│ └── disabled-example.component.ts (template inline)
└── inline-html.html (mode 2 — bare HTML) Authoring rules
-
Title (text between
@playgroundand the optional path) is required. Path-only tags (no title) are dropped with a build-time warning. - Mutual exclusion: a tag with both a fileRef AND a fenced body is dropped with a “mutually exclusive” warning. Pick one.
-
File paths must be relative (
./or../prefix), end in.htmlor.ts, and resolve against the JSDoc’s host file. -
templateUrl,styleUrl,styleUrlsin the entry must use string literals — template literals are not parsed. -
Titles can contain slashes (e.g. “A / B comparison”) — only a trailing
path with
.html/.tsextension triggers file-ref mode. - The Playground tab is component-only. Other entity types ignore the tag.
-
Runs alongside
@exampleon the Info tab and<example-url>on the Example tab. All three can coexist on the same component.
@playground vs. @example vs. <example-url>
Three tags, three intents:
-
@example: static, syntax-highlighted snippet on the Info tab. Use for code readers should copy. -
<example-url>: iframe demo of an externally hosted page on the Example tab. Use when you already host the demo. -
@playground: runnable StackBlitz project assembled from your component’s source. Use when you want readers to fork and try.
How the manifest is built
At build time compodocx assembles a StackBlitz WebContainer-templated (template: 'node') Angular CLI 21 project per block: a minimal main.ts,
app.component.ts that hosts your snippet (or IS your TS-mode entry),
the documented component’s source plus any transitive imports, and a
package.json with the dependency table.
The manifest’s dependencies map starts with the eight Angular
peers (@angular/core,
@angular/common, @angular/compiler,
@angular/forms, @angular/router,
@angular/animations, and the
platform-browser pair) all aligned on a single major derived from your
@angular/core. Non-Angular runtime peers
zone.js, rxjs,
tslib are always present.
@angular/material and @angular/cdk are
forwarded only when your package.json declares them OR when Material
auto-detection scans recognise widget selectors in your demo.
Beyond those tables, every bare-specifier import seen in any walked source or in your snippet is
auto-forwarded with the version your package.json declares. Missing
roots are skipped silently. The
playgroundDependencies config-only key (no CLI flag) lets you inject
extra packages or pin a specific version per build — useful for libraries the consumer ships
but doesn’t install directly.
Dependency walking is bounded: depth 3, 25 files maximum, 8000 characters per file. When the
caps trip, the section renders a static fallback instead of a launcher and the build log
explains why. @internal-tagged exports are filtered out before
serialisation, so private code never leaks into a public StackBlitz.
Library-author workflow
Component-library authors who want first-class runnable demos — the way Angular Material
ships them — pair compodocx with a sibling dev-app inside the workspace. The library lives
at projects/<library>/; a dev-app at
projects/dev-app/ imports the SAME example components compodocx embeds
in the docs. Single source of truth, no copy-paste drift.
The example folder layout sits inside each component:
projects/ui-kit/src/lib/button/
├── button.component.ts ← @playground tags
├── examples/
│ ├── default/
│ │ ├── default-example.component.ts ← imported by both compodocx AND dev-app
│ │ ├── default-example.component.html
│ │ └── default-example.component.css
│ └── states-showcase.html ← Mode 2 sibling
projects/dev-app/src/app/app.component.ts:
import { DefaultExample } from '@my-org/ui-kit/button/examples/default/default-example.component';
// composed alongside other example components for local preview
Run compodocx against the library’s tsconfig (not the dev-app), and pin the
library’s version in playgroundDependencies if your published
package should appear in the StackBlitz package.json alongside the inline
sources:
{
"tsconfig": "projects/ui-kit/tsconfig.lib.json",
"output": "docs/",
"publicApiOnly": true,
"playgroundDependencies": {
"@my-org/ui-kit": "^1.0.0"
}
} See the options guide for every flag and config key that touches the playground pipeline.
Opt-out
Hide the tab globally with --disablePlaygroundTab or
disablePlaygroundTab: true in your config file. Independent of
--disableDependenciesTab: one suppresses the per-component
standalone-import graph, the other suppresses the Playground tab. The flag has no effect on
components that declare no
@playground blocks (the tab is already absent).
Browser path
Each block emits an inert manifest into the static HTML as
<script type="application/json" data-cdx-stackblitz-manifest-data>. On click, the launcher dynamic-imports @stackblitz/sdk. The SDK
ships as a separate ~7 KB gzipped chunk that is never loaded until needed. The SDK call is
openProject(manifest, { newWindow: true }): a fresh tab
opens, no iframe overlays the doc page.