Skip to content
Run BunYour friendly Bun playground
Canary build: these docs may describe unreleased changes. Read this page on docs.runbun.dev

Embed API

Version 1. For how to use it, see Embedding. The element comes from https://runbun.dev/embed.js; the page it wraps is https://runbun.dev/embed.

The element’s attributes and the iframe’s URL parameters share names.

NameValuesDefaultNotes
modecode, preview, console, full, resultcodeWhich panes show. See Features.
featurescomma listnoneAdded to the mode’s features. -name turns one off and locks it.
filepaththe entry fileThe file opened first.
pathURL path/What the preview requests first, e.g. /user/123.
autoruntrue, falsetrue, except in fullRun once loaded, after installing any missing packages.
themeauto, light, dark, or a theme family’s light variantautoThe color mode. e.g. dark.
familya theme family’s light variantnonee.g. github-light. Picks both halves of the family without changing the color mode, so theme can follow your page.
fontsstylesheet URLs, whitespace-separatednoneLoaded inside the embed, for the fonts --font-ui and --font-mono name.
varsJSON objectnoneChrome CSS variables to set on top of the theme’s. Element only; see Theming.
readonlybooleanfalseThe editor takes no edits. Run still works.
persistkeynoneKeeps edits in the reader’s browser under this key. Without it every load starts from the source.
expandempty, a mode, falsenoneA button that opens the embed in a modal over the page. With a mode (expand="full") it switches to that mode while open. Escape or the button closes it.
statustoast, bartoastWhere the run status shows: a toast over the panes’ bottom-right corner that fades a few seconds after it settles (errors stay), or a pill in the bar.
examplenamenoneA bundled example.
repoowner/repo[@ref]noneA public GitHub repo.
share#code=…noneA share link’s hash. In an iframe URL put it after #.
loadeager, visible, clickvisibleElement only. When the embed starts.
heightCSS length, autocode: auto; console: 360px; preview: 420px; full: 600px; result: 420pxElement only.
srcURLwhere embed.js came fromElement only. Another playground base URL.
titletextnoneThe iframe’s title, for assistive technology.

A boolean is on unless it is false or 0. Unknown or malformed values fall back to the default.

Inline files: child <script type="text/plain" data-file="path"> elements, dedented. Poster: children with slot="poster", shown until the embed is ready; with load="click" a Run button over them starts it.

<bun-playground-defaults> sets defaults for every <bun-playground> on the page. It takes the same attribute names and values, and renders nothing. Put it before the embeds.

<bun-playground-defaults family="github-light" status="bar" load="click" fonts="https://fonts.googleapis.com/css2?family=Inter&display=swap"></bun-playground-defaults>
  • Keys: mode, features, autorun, theme, family, fonts, readonly, expand, status, vars, load, height, src. The others (file, path, example, repo, share, title, persist) are per embed; a shared persist key would make embeds overwrite each other’s edits.
  • Precedence: built-in default, then the page default, then the element’s attribute, then what the reader picks in the embed’s menu. An element opts out of a boolean default with false, e.g. readonly="false". Removing an element’s attribute falls back to the page default.
  • vars merges per scheme: the element’s base, light and dark go over the page default’s. Every other key replaces the default, features too. Malformed vars JSON in either place is skipped.
  • Changing theme, family, vars, mode or height on the defaults element updates running embeds without reloading them. The other keys only reach embeds that haven’t started yet.
  • The first <bun-playground-defaults> in the document wins; with more than one the element logs a warning. It is looked up every time, so a page that swaps its content (client-side navigation) can swap it too.

mode picks a preset; features adjusts it.

NameWhatOn inCost
filesFile treefull
tabsTab bar for open filespreview, full
consoleConsole outputcode, console, full
previewPreview of a serverpreview, full, result
typesType errors, hover and completionsnone10 MB on first use
formatFormat button (bun fmt)full7 MB on first use
installInstall buttonpreview, full
terminalInteractive terminalconsole, full
open“Open in runbun.dev”all
watchRestart the running program when an edit is saved, like bun --watchall
autosaveAn Auto-save switch over the editor: off, edits save on Ctrl/Cmd+S and before Run (switching it back on, or turning the feature off, saves what’s waiting). Without it, edits save as you typenone

Readers can switch features on or off from the ⋯ menu, except those you locked with -name.

On the element, each returns a promise. Call them after await el.ready.

MethodParamsResult
run(entry?)file to start instead of the entryresolves when started
stop()
install()resolves when done
test(files?)file paths{ failed }
readFile(path)paththe file’s text (bytes for a binary file)
writeFile(path, text)path, text
listFiles()array of paths
openFile(path)path
setMode(mode)a mode
setFeatures(obj){ types: true, format: false }
setTheme(theme)a theme
setFamily(theme)a theme family’s light variantpicks both halves without changing the color mode
setVars(vars)a vars objectsets the chrome’s CSS variables
getShareUrl()the share link as a string
activate()Starts an embed waiting for load="click" or visible.
expand()Opens the modal (works without the expand button too).
collapse()Puts the embed back in the page.

el.expanded is true while the modal is open.

el.ready resolves once the embed has sent ready (or unsupported); read the data from the bun-playground:ready event. Requests sent earlier wait until the project has loaded.

On the element they are DOM events named bun-playground:<event>, with the data as detail. Over postMessage they have the same names without the prefix.

EventData
ready{ version, bun, canRun, mode, features }
unsupported{ checks: { name, ok, why }[] }: the browser lacks something, so nothing runs; the code still shows and edits
status{ state: "idle" | "busy" | "running" | "error", text }
console{ level, text }
change{ path, removed }
test{ failed }
mode{ mode, features }: after setMode, setFeatures or the reader’s menu
resize{ height }: the content’s height in pixels, for iframes
expand{ open }: on the element, the modal opened or closed. Over postMessage, the reader pressed the expand button (or Escape while open); the host opens or closes its modal and answers with setExpanded
boot{ version }: postMessage only; see below

Use this with a bare iframe. Every message in either direction is an object with the key bunPlayground: 1. Others are ignored.

Host to embed:

iframe.contentWindow.postMessage({ bunPlayground: 1, id: 1, method: "writeFile", params: { path: "index.ts", text: "…" } }, "https://runbun.dev");

The embed answers with the same id: { bunPlayground: 1, id: 1, result } or { bunPlayground: 1, id: 1, error: "message" }. Methods and params are those above, plus init and setExpanded ({ open }: tell the embed the host’s modal opened or closed, so it switches mode and its button follows).

Embed to host, unsolicited:

{ bunPlayground: 1, event: "ready", data: { version: 1, bun: "…", canRun: true, mode: "code", features: ["console", "open"] } }

With ?init=1 in its URL, the page sends boot as soon as it listens, then waits for an init request before loading a project (the element always works this way):

window.addEventListener("message", (e) => {
if (e.data?.bunPlayground === 1 && e.data.event === "boot") {
iframe.contentWindow.postMessage(
{ bunPlayground: 1, id: 1, method: "init", params: { files: { "index.ts": "console.log(1)" }, options: { mode: "code", readonly: true } } },
"https://runbun.dev",
);
}
});

files maps paths to text. options takes mode, features, file, path, autorun, theme, family, fonts, readonly, persist, expand and status. vars takes the object described under Theming. Without ?init=1, the page loads from its URL and init isn’t needed; setFamily and setVars change both afterwards.

A theme family (theme’s variant or family) colors the editor and the app chrome. vars overrides chrome values on top of the theme, as an object with base (both schemes) and optionally light or dark:

{
"base": { "--accent": "#ff5a5f", "--radius": "1rem" },
"light": { "--bg": "#ffffff" },
"dark": { "--bg": "#171723" }
}

Names and values are CSS custom properties; anything that isn’t a --name with a string value is ignored. The chrome variables are:

NameWhat
--bgThe code surface
--canvasBehind the panes
--panelThe bar, tabs and file tree
--fgText
--mutedSecondary text
--borderLines between panes
--accentSelection, focus rings, the Run button
--error, --warn, --info, --okStatus colors
--radius, --padA pane’s corner and the space inside it
--font-ui, --font-monoThe UI and code fonts; load them with fonts
--control, --control-hover, --control-borderButtons and inputs

Others in style.css work too, but these are the ones a page usually wants to match its own design. To follow a page’s light and dark themes, send base for the scheme in effect and update it (with setVars) when the page changes.

To draw over the embed (a border, a glow), style bun-playground::part(overlay): an empty layer covering the poster and the frame, in the modal too, that lets clicks through.

The poster (shown until the embed is ready) is styled by CSS custom properties set on <bun-playground> or an ancestor:

NameWhatDefault
--poster-bg, --poster-fgThe poster’s background and textCanvas, CanvasText
--font-monoThe default poster’s code font (load="click" without a slot="poster")ui-monospace, …
--play-bg, --play-fgThe Run button’s background and text; --play-bg also colors the spinner shown while the embed startsButtonFace, ButtonText
--play-bg-hoverThe Run button’s background on hover--play-bg mixed 15% toward --play-fg
--play-border, --play-radius, --play-fontThe Run button’s border color, corner radius and fontGrayText, 6px, inherited

If the embed isn’t ready after 60 seconds, the spinner turns back into the button, as “Try again”.

This is version 1. Changes only add: new methods, events, options, features and fields. Nothing is renamed or removed within a version. Ignore message keys and events you don’t know; the embed does the same.