Summary
Add a new @studiometa/playground-preview package that provides a <playground-preview> custom element to embed playground previews anywhere (docs sites, blogs, READMEs, any HTML page) — framework-agnostic.
This is inspired by the PreviewPlayground.vue component from @studiometa/ui docs, but as a standalone web component with zero framework dependency.
API
Short content via attributes
<playground-preview html="<h1>Hi</h1>" script="console.log(1)"></playground-preview>
Long content via <script> children
<playground-preview height="80vh" theme="dark">
<script type="playground/html">
<div class="flex items-center gap-4">
<h1>Hello World</h1>
</div>
</script>
<script type="playground/script">
import { Base, createApp } from '@studiometa/js-toolkit';
class App extends Base {
static config = { name: 'App' };
mounted() { console.log('mounted!'); }
}
export default createApp(App);
</script>
<script type="playground/css">
@import "tailwindcss";
h1 { @apply text-4xl font-bold; }
</script>
</playground-preview>
Children <script type="playground/..."> take precedence over attributes when both are provided.
Attributes
| Attribute |
Type |
Default |
Description |
html |
string |
"" |
HTML content (short inline) |
script |
string |
"" |
JavaScript content (short inline) |
css |
string |
"" |
CSS content (short inline) |
base-url |
string |
https://studiometa-playground.pages.dev |
Playground instance URL |
height |
string |
60vh |
Container height |
zoom |
number |
0.9 |
Initial iframe scale |
theme |
string |
auto |
dark | light | auto (uses prefers-color-scheme) |
no-controls |
boolean |
false |
Hide zoom/reload/open controls |
header |
string |
— |
Passed through to playground URL |
Exports
// Auto-register — side-effect import, defines <playground-preview>
import '@studiometa/playground-preview';
// Manual registration — class only
import { PlaygroundPreview } from '@studiometa/playground-preview/element';
customElements.define('my-playground', PlaygroundPreview);
Design decisions
Separate package
The web component is a thin shell (iframe + zip + controls). It should be ~5KB bundled. Keeping it separate from @studiometa/playground avoids forcing consumers to install heavy deps (esbuild-wasm, Monaco, webpack-config, etc.) just for an embed widget.
Shadow DOM
Style encapsulation ensures the component's controls and layout are not affected by the host page's CSS. Theming is exposed via CSS custom properties:
playground-preview {
--pg-bg: #f5f5f5;
--pg-controls-bg: rgba(0, 0, 0, 0.6);
--pg-controls-color: white;
--pg-border-radius: 8px;
}
Controls
- Zoom in / out / reset
- Reload iframe
- Open in new window (links to full playground with editors enabled)
Lazy loading
Uses IntersectionObserver to defer iframe creation until the component scrolls into view. Shows a loader overlay while the iframe is loading.
Observed attributes & reactivity
html, script, css, theme, base-url, header → rebuild iframe URL
height, zoom → update styles only
no-controls → toggle controls visibility
<script type="playground/..."> children are read once at connectedCallback
URL building
Params are placed in the hash (not query string) to match the existing playground URL format and avoid server-side issues with long URLs:
{base-url}/#html=<zipped>&script=<zipped>&style=<zipped>&html-editor=false&script-editor=false&style-editor=false&theme=dark
The "open in new window" link uses the same URL but without *-editor=false so editors are visible.
Build
- ESM only output
- esbuild (same approach as the main package)
- Single dependency:
fflate (for zip compression)
Package structure
packages/playground-preview/
package.json
tsconfig.json
tsconfig.build.json
src/
index.ts # re-exports class + auto-defines the element
element.ts # exports the PlaygroundPreview class only
playground-preview.ts # the custom element implementation
zip.ts # zip util (uses fflate)
styles.ts # shadow DOM CSS as template literal
icons.ts # inline SVG icon strings
Tasks
Summary
Add a new
@studiometa/playground-previewpackage that provides a<playground-preview>custom element to embed playground previews anywhere (docs sites, blogs, READMEs, any HTML page) — framework-agnostic.This is inspired by the
PreviewPlayground.vuecomponent from@studiometa/uidocs, but as a standalone web component with zero framework dependency.API
Short content via attributes
Long content via
<script>childrenChildren
<script type="playground/...">take precedence over attributes when both are provided.Attributes
htmlstring""scriptstring""cssstring""base-urlstringhttps://studiometa-playground.pages.devheightstring60vhzoomnumber0.9themestringautodark|light|auto(usesprefers-color-scheme)no-controlsbooleanfalseheaderstringExports
Design decisions
Separate package
The web component is a thin shell (iframe + zip + controls). It should be ~5KB bundled. Keeping it separate from
@studiometa/playgroundavoids forcing consumers to install heavy deps (esbuild-wasm, Monaco, webpack-config, etc.) just for an embed widget.Shadow DOM
Style encapsulation ensures the component's controls and layout are not affected by the host page's CSS. Theming is exposed via CSS custom properties:
Controls
Lazy loading
Uses
IntersectionObserverto defer iframe creation until the component scrolls into view. Shows a loader overlay while the iframe is loading.Observed attributes & reactivity
html,script,css,theme,base-url,header→ rebuild iframe URLheight,zoom→ update styles onlyno-controls→ toggle controls visibility<script type="playground/...">children are read once atconnectedCallbackURL building
Params are placed in the hash (not query string) to match the existing playground URL format and avoid server-side issues with long URLs:
The "open in new window" link uses the same URL but without
*-editor=falseso editors are visible.Build
fflate(for zip compression)Package structure
Tasks
packages/playground-previewpackagePlaygroundPreviewcustom element<script type="playground/...">child content supportpackage.json