Code Playground

Edit a component's code and HTML side by side, with a live sandboxed preview.

zweiundeins <sb-code-playground> code playground sandbox editor preview repl Since 2026-09-21 15.3 kB0Open in playground Edit on GitHub

Playground

A small code sandbox: file tabs with sb-code-editor, a live preview in a sandboxed iframe, a theme picker and a console. Every run gets a fresh document, because custom elements can't be redefined. The user code runs in an opaque origin, so it can't touch the host page's cookies, storage or DOM.

It needs sb-code-editor on the page and a runner page for the iframe. This site's runner lives at /playground/run. The protocol is below if you want to host your own.

Examples

Files from the markup

Give it child <script type="text/plain" data-file="…"> elements: component.js, index.html and optionally style.css. They are never parsed or executed on the host page.

<sb-code-playground style="inline-size: 100%; --sb-code-playground-height: 22rem">
  <script type="text/plain" data-file="component.js">
    import { rocket } from 'datastar'

    rocket('sb-greeting', {
      props: ({ string }) => ({ name: string.default('astronaut') }),
      render: ({ html, props: { name } }) => html`<p>Hello, ${name}! 🚀</p>`,
    })

    console.log('defined <sb-greeting>')
  </script>
  <script type="text/plain" data-file="index.html">
    <sb-greeting name="Starbase"></sb-greeting>
  </script>
</sb-code-playground>

Using other components

deps maps tags to module URLs to load in the preview. Tags that the edited code defines itself are skipped, so you can edit a component that its own demo also uses.

Runner protocol

The iframe (sandbox="allow-scripts") loads runner and exchanges postMessages. Every message carries source: "sb-runner".

Direction Message
runner → host {type: "ready"}
host → runner {type: "run", files, deps: [url], theme, base}
runner → host {type: "console", level, args} · {type: "error", message, line} · {type: "done"}

The runner should import the edited component.js (for example from a blob: URL) before the dependencies, then datastar, and resolve 'datastar' through an import map. A blob: module has no folder, so the runner resolves relative imports (./vendor/lib.js) against base, the base attribute as an absolute URL (empty when unset).

Accessibility

File tabs are real tabs, the editors are native textareas, the preview frame has a title, and the console is a role="log" live region. Ctrl/Cmd+Enter in an editor runs the code.

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.

<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
     It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>

<sb-code-playground style="--sb-code-playground-height: 8.5rem; inline-size: 100%"><script type="text/plain" data-file="index.html"><sb-button variant="pixel">Hi!</sb-button></script></sb-code-playground>

<!-- In production, pin today's catalog instead of the latest: the browser then
     refuses any file that changed. Add "integrity" to the import map above: the
     hashes from https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/autoloader.js" integrity="sha384-IvSCUjEWPS+wwXunU+48x96KmyWDx/Y4LeCwCL8cWFqJ/zHmTsTOsSMrzTOcpW3D"></script>
-->

<!-- Or load just this component, pinned to this version: -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/code-playground@023a7d0bb3c2/code-playground.js" integrity="sha384-yeMaB/sA59n3kvmtkerl6O7aJWP4KyFSIPMOlb9TmH+Q8mkLqsLihWWQiVigCXDs"></script> -->

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotli
code-playground.js 10.6 kB4.2 kB3.6 kB
<sb-code-editor> renders it34.8 kB13.2 kB11.8 kB
Total45.5 kB17.4 kB15.3 kB

API reference

Props

AttributeTypeDefaultDescription
runnerstring"/playground/run"URL of the sandbox runner page (see the protocol in the docs).
depsjson{}Other components to load in the preview, as JSON {"sb-tag": "module URL"}. Tags defined by the edited code are skipped.
auto-runbooleantrueRe-run shortly after every edit.
themestring"deep-space"Initial preview theme (data-sb-theme).
themesarray[ "deep-space", "nebula", "terminal", "daylight" ]Themes offered in the picker.
delaynumber600Auto-run debounce, in ms.
basestring""URL that relative imports in component.js resolve against (the folder its vendored files are served from).
initialjson{}Initial files as JSON {"component.js": "…"}; handy for server-rendered pages. Child scripts are used when empty.

Slots

NameDescription
(files)Child <script type="text/plain" data-file="component.js|index.html|style.css"> elements with the initial files.

Events

NameDescription
sb-changeAfter an edit. detail: { files }.
sb-runWhen the preview is (re)started. detail: { files }.