Version v2.1.0 Size 3.3 KB gzip Dependencies Zero dependencies
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.
ts
// Before
container.innerHTML = untrustedHtml;
// After
const sandbox = createSandbox(container);
await sandbox.render(untrustedHtml);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.3 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.
Consider 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);
try {
// render() 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');
} catch (error) {
console.error('Sandbox render failed', error);
}
// 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 rendersreplaceBody(html)— Replace body descendants without navigating; head scripts/styles survive while descendant state is replaced; suited to host-owned streaming markupupdateStyle(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 basic language tag and<title>on generated documents 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 static isolated sandbox markup for server-side or offline use; usecreateSandbox()for host-managed runtime controls- Error forwarding —
onerror+unhandledrejectionforwarded as{ type: 'error' }messages - Disposable —
dispose()+[Symbol.dispose]forusingdeclarations