Why Sandbox?
Running untrusted HTML in the main window is unsafe — arbitrary code can access the DOM, cookies, and user data. Sandbox creates an isolated <iframe sandbox="allow-scripts"> that receives content over a typed postMessage bridge. The sandbox cannot reach the host page.
Common use cases:
- Component previews — render isolated HTML/CSS examples in documentation or design tools
- Code playgrounds — execute user-provided code with full error forwarding and state injection
- Plugin sandboxes — host third-party or user-authored plugin UI without granting host access
- User-generated content — display untrusted HTML (emails, form output, external widgets) safely
- Widget embedding — wrap third-party widgets with strict CSP and bidirectional messaging
- AI-generated UI — render LLM-produced HTML components with guaranteed isolation
| Feature | Raw <iframe> | Sandbox |
|---|---|---|
| Bundle size | 0 B (built-in) | 3.2 KB |
| Zero dependencies | ||
| Content-Security-Policy | Manual | Auto-generated, strict by default |
| Typed postMessage protocol | setState() / SandboxMessage union | |
| Error forwarding | onerror + unhandledrejection → host | |
Dispose / using | Manual remove() | dispose() + [Symbol.dispose] |
Use Sandbox when you need to render untrusted or user-provided HTML in the browser with guaranteed isolation, CSP enforcement, and a typed event bridge.
Stick with a raw <iframe> when you only need to embed a known third-party URL — Sandbox is for programmatic srcdoc content, not URL-based embedding.
Installation
sh
pnpm add @vielzeug/sandboxsh
npm install @vielzeug/sandboxsh
yarn add @vielzeug/sandboxQuick Start
ts
import { createSandbox } from '@vielzeug/sandbox';
const container = document.getElementById('preview')!;
const sandbox = createSandbox(container);
// render() returns a Promise that resolves when the document is ready
await sandbox.render('<ore-button variant="primary">Click me</ore-button>');
// Push state into the sandbox
sandbox.setState('theme', 'dark');
// Receive events from sandbox code (ready is not forwarded — internal use only)
sandbox.onMessage((msg) => {
if (msg.type === 'custom') console.log(msg.event, msg.detail);
if (msg.type === 'error') console.error(msg.message);
if (msg.type === 'resize') console.log('height:', msg.height);
});
// Re-render: await the returned Promise
await sandbox.render(newHtml);
// Clean up — removes iframe, clears listeners
sandbox.dispose();
// or: using sandbox = createSandbox(container);Features
createSandbox()— Creates an isolated<iframe sandbox="allow-scripts">in the given containerSandboxHandle.ready— Promise resolving on first render's ready signal (also resolves on dispose; checksandbox.disposedto distinguish)SandboxHandle.disposalSignal—AbortSignalaborted when the sandbox is disposed; tie async work to sandbox lifetimeSandboxHandle.disposed— Observable disposed state; check before deferred callsrender(html, { signal? })— Lazy iframe creation; returnsPromise<void>resolving when ready, or rejecting withSandboxTimeoutErrorif the bridge never signals ready; passAbortSignalto skip cancelled renderspatch(html)— Incremental body update without page reset; preserves scripts, listeners, and CSS state; ideal for streaming contentupdateStyle(id, css)— Hot-patch a named<style id="…">block live without re-rendering; also updates baseline for next rendersetState(key, value)— Push state into the sandbox; received assandbox:state-updateCustomEventsetStateAll(record)— Push multiple state values in a single postMessage; more efficient than repeatedsetState()calls for initial setupnamedStylesoption — Named<style id="key">blocks in document<head>; individually patchable viaupdateStyle()lang/titleoptions — Set<html lang="…">and<title>on the generated document for screen-reader correctnessSandboxBridgetype — Ambient type forwindow.__sandbox__in sandbox-side TypeScript;onState(key, handler)subscribes to state pushed viasetState()/setStateAll()custommessages — Sandbox code emitswindow.__sandbox__.emit(event, detail)to the hostresizemessages — Auto-emitted by the bridge's built-inResizeObserver; no manual wiring needed- Strict CSP —
default-src 'none', inline scripts only, no network by default nonceoption — Cryptographic nonce for bridge<script>tag andscript-srcCSPscriptsoption — Inject CDN scripts withcrossorigin="anonymous"; origins auto-added toscript-srcbuildCsp()— Build a standalone CSP string using the sameSandboxOptionsbuildDocument()— Build a complete sandbox HTML document for server-side or offline use- Error forwarding —
onerror+unhandledrejectionforwarded as{ type: 'error' }messages - Disposable —
dispose()+[Symbol.dispose]forusingdeclarations