Skip to content
compodocx
Skip to article content

Guide 07

Playground Blocks

cngxjs (opens in new tab)Updated Guide07of 13

Built onStackBlitzAssembled at build time · launches on click

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, and nothing contacts StackBlitz until a reader clicks.

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@Component class 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.

ts
/**
 * 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.

ts
/**
 * Reusable button.
 *
 * @playground Showcase ./examples/button-showcase.html
 */
@Component({ /* ... */ })
export class MyButton { /* ... */ }
htmlbutton-showcase.html
<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.

ts
/**
 * Reusable button.
 *
 * @playground Counter ./examples/counter/counter-example.component.ts
 */
@Component({ /* ... */ })
export class MyButton { /* ... */ }
tscounter-example.component.ts
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 appendsexport { YourClass as AppComponent } automatically so the import in src/main.ts resolves.

Recommended folder layout for a library component with multiple demos:

text
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 @playground and 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 .html or.ts, and resolve against the JSDoc’s host file.
  • templateUrl, styleUrl,styleUrls in 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 @example on 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 project per block, on the Angular version yourpackage.json declares (^21.0.0 when it declares none): 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 apackage.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 theplatform-browser pair) all aligned on a single major derived from your@angular/core. Non-Angular runtime peerszone.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. TheplaygroundDependencies 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 by default: depth 3, 25 files maximum, 8000 characters per file. Raise the caps with playgroundDepDepth,playgroundFileCountCap, andplaygroundFileCap. Going over the file count fails that playground with a message naming the walked files, and an oversized file is truncated with a footer.@internal-tagged exports are filtered out before serialisation, so private code never leaks into a public StackBlitz.

Run against your working tree

A registry dependency resolves to the last published version, so a demo that uses unreleased code fails in the sandbox. playgroundVendor embeds your locally built packages into the project instead, slimmed and pruned to the entry points the demo reaches. Build the libraries before running compodocx. For providers or routing, point a component-level@playgroundConfig ./app.config.ts tag at a file that exportsappConfig.

Catch broken demos before you ship

Each build warns when a demo imports a subpath or symbol the pinned package version doesn’t have;--strictPlaygrounds turns that warning into a failure.compodocx playground:validate <docsDir> goes further and compiles every playground with a real install and build.

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 atprojects/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:

text
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:

json.compodocxrc.json
{
  "tsconfig": "projects/ui-kit/tsconfig.lib.json",
  "output": "docs/",
  "publicApiOnly": "dist/ui-kit",
  "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 ordisablePlaygroundTab: true in your config file. Independent of--disableDependenciesTab: one suppresses the per-component dependency view, 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 calls the StackBlitz SDK that ships inside the site script:openProject(manifest, { newWindow: true }). A fresh tab opens, no iframe overlays the doc page.