Client Setup
Mount HyperGen iframes in your frontend — React, Vue, Svelte, or vanilla JS. Manage themes, handle resize, and control the lifecycle.
The client side of HyperGen mounts a sandboxed iframe, injects the bootstrap document, and gives you a controller for managing the iframe lifecycle. Everything you need is in a single file: hypergen-client.ts.
API Overview
| Export | Purpose |
|---|---|
mountHyperGen(container, options) | Create a sandboxed iframe and return a HyperGenController |
mountHyperGen()
Creates a sandboxed iframe, loads the bootstrap document, sets up postMessage listeners, and returns a controller.
import { mountHyperGen } from "./hypergen-client";
const controller = mountHyperGen(document.getElementById("agent-ui")!, {
bootstrapUrl: "/bootstrap",
});Options
| Option | Type | Default | Description |
|---|---|---|---|
bootstrapUrl | string | -- | URL that serves the bootstrap HTML (iframe src) |
bootstrapHtml | string | -- | Inline bootstrap HTML (iframe srcdoc) |
theme | Record<string, string> | -- | Initial CSS variables sent after iframe loads |
iframeOrigin | string | "*" | Expected iframe origin for postMessage validation |
autoResize | boolean | true | Auto-resize iframe based on content height |
maxHeight | number | 2000 | Maximum iframe height in pixels |
minHeight | number | 100 | Minimum iframe height in pixels |
extraSandboxPermissions | string[] | [] | Additional sandbox permissions |
className | string | -- | CSS class for the iframe element |
style | Partial<CSSStyleDeclaration> | -- | Inline styles for the iframe element |
You must provide either bootstrapUrl or bootstrapHtml (not both).
bootstrapUrl -- The iframe loads the bootstrap document over HTTP. This is the standard approach: the iframe's src attribute is set to this URL.
bootstrapHtml -- The bootstrap HTML is injected directly via srcdoc. Use this when you generate the bootstrap server-side and want to avoid an extra HTTP request (e.g., in SSR frameworks).
// URL approach (extra HTTP request, simpler)
const ctrl = mountHyperGen(el, { bootstrapUrl: "/bootstrap" });
// Inline HTML approach (no extra request, requires server-side rendering)
const ctrl = mountHyperGen(el, { bootstrapHtml: serverGeneratedHtml });HyperGenController
The object returned by mountHyperGen(). Use it to manage the iframe after mounting.
controller.setTheme(vars)
Sends CSS variables to the iframe via postMessage. The iframe's bridge script applies them to :root.
controller.setTheme({
"--hg-surface": "#1a1a2e",
"--hg-text": "#e0e0e0",
"--hg-accent": "#7c3aed",
});Call this any time to update the theme (e.g., when the user toggles dark mode). Only the provided variables are updated -- existing variables are not removed.
controller.onResize(callback)
Registers a callback for content size changes. The iframe's ResizeObserver detects height changes and posts them via postMessage.
const unsubscribe = controller.onResize((height, width) => {
console.log(`Content: ${height}px tall, ${width}px wide`);
});
// Later: stop listening
unsubscribe();When autoResize is true (the default), the iframe element's height is automatically adjusted within the minHeight/maxHeight bounds. The callback fires regardless of the autoResize setting.
controller.onNavigate(callback)
Registers a callback for navigation requests from the iframe. Agents trigger navigation by including elements with data-hg-navigate attributes.
const unsubscribe = controller.onNavigate((url) => {
// Integrate with your router
router.push(url);
});Inside the iframe, an agent can generate:
<a data-hg-navigate="/dashboard" href="#">Go to Dashboard</a>Clicking it sends a hg:navigate postMessage instead of navigating inside the iframe.
controller.onData(callback)
Registers a callback for arbitrary data sent from the iframe. Agents can dispatch custom events that the bridge forwards to the host.
const unsubscribe = controller.onData((payload) => {
console.log("Data from agent:", payload);
});Inside the iframe, agent-generated JavaScript can send data:
<script>
document.dispatchEvent(new CustomEvent("hg:data", {
detail: { action: "selected", itemId: 42 }
}));
</script>controller.destroy()
Tears down the iframe: sends hg:destroy to close SSE connections, removes the message listener, clears all subscriptions, and removes the iframe from the DOM.
controller.destroy();
console.log(controller.destroyed); // trueAlways call destroy() when the component unmounts to prevent memory leaks.
controller.iframe
Direct access to the underlying HTMLIFrameElement for advanced use cases.
controller.iframe.style.opacity = "0.5";controller.destroyed
Boolean indicating whether destroy() has been called.
Framework Integration
React
import { useEffect, useRef } from "react";
import { mountHyperGen, type HyperGenController } from "./hypergen-client";
function AgentUI({ theme }: { theme?: Record<string, string> }) {
const containerRef = useRef<HTMLDivElement>(null);
const controllerRef = useRef<HyperGenController | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const controller = mountHyperGen(containerRef.current, {
bootstrapUrl: "/bootstrap",
theme,
});
controllerRef.current = controller;
controller.onNavigate((url) => {
// Use your router: window.location.href = url
});
return () => controller.destroy();
}, []);
// Update theme when props change
useEffect(() => {
if (theme && controllerRef.current && !controllerRef.current.destroyed) {
controllerRef.current.setTheme(theme);
}
}, [theme]);
return <div ref={containerRef} />;
}Vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from "vue";
import { mountHyperGen, type HyperGenController } from "./hypergen-client";
const props = defineProps<{
theme?: Record<string, string>;
}>();
const container = ref<HTMLElement | null>(null);
let controller: HyperGenController | null = null;
onMounted(() => {
if (!container.value) return;
controller = mountHyperGen(container.value, {
bootstrapUrl: "/bootstrap",
theme: props.theme,
});
controller.onNavigate((url) => {
// useRouter().push(url)
});
});
watch(() => props.theme, (newTheme) => {
if (newTheme && controller && !controller.destroyed) {
controller.setTheme(newTheme);
}
});
onUnmounted(() => {
controller?.destroy();
});
</script>
<template>
<div ref="container" />
</template>Svelte
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { mountHyperGen, type HyperGenController } from "./hypergen-client";
export let theme: Record<string, string> | undefined = undefined;
let container: HTMLElement;
let controller: HyperGenController | null = null;
onMount(() => {
controller = mountHyperGen(container, {
bootstrapUrl: "/bootstrap",
theme,
});
controller.onNavigate((url) => {
// goto(url)
});
});
onDestroy(() => {
controller?.destroy();
});
$: if (theme && controller && !controller.destroyed) {
controller.setTheme(theme);
}
</script>
<div bind:this={container}></div>Vanilla JS
<div id="agent-ui"></div>
<script type="module">
import { mountHyperGen } from "./hypergen-client.js";
const controller = mountHyperGen(
document.getElementById("agent-ui"),
{ bootstrapUrl: "/bootstrap" }
);
controller.setTheme({
"--hg-accent": "#7c3aed",
"--hg-surface": "#ffffff",
});
controller.onResize((height) => {
console.log("Content height:", height);
});
// Clean up on page unload
window.addEventListener("beforeunload", () => {
controller.destroy();
});
</script>Auto-Resize Behavior
By default, the iframe automatically resizes to match its content height. The flow:
- The iframe's bootstrap document includes a
ResizeObserveron<body> - When content changes size, the observer posts a
hg:resizemessage to the host mountHyperGen()receives the message and adjustsiframe.style.height- The height is clamped between
minHeightandmaxHeight
The resize transition is smooth (150ms ease-out) to prevent visual jitter.
To disable auto-resize and handle it yourself:
const controller = mountHyperGen(el, {
bootstrapUrl: "/bootstrap",
autoResize: false,
});
controller.onResize((height) => {
// Custom resize logic
el.style.height = `${Math.min(height, 800)}px`;
});Multiple Iframes on One Page
You can mount multiple HyperGen iframes, each with its own controller and SSE connection:
const chatCtrl = mountHyperGen(
document.getElementById("chat")!,
{ bootstrapUrl: "/bootstrap?view=chat" },
);
const dashCtrl = mountHyperGen(
document.getElementById("dashboard")!,
{ bootstrapUrl: "/bootstrap?view=dashboard" },
);
// Each controller is independent
chatCtrl.setTheme({ "--hg-accent": "#10b981" });
dashCtrl.setTheme({ "--hg-accent": "#f59e0b" });Each iframe maintains its own SSE connection, theme, and lifecycle. The mountHyperGen() implementation verifies that incoming postMessage events originate from the correct iframe (event.source === iframe.contentWindow), so messages never cross between instances.
Sandbox Permissions
The default sandbox is allow-scripts allow-same-origin allow-forms -- the minimum HTMX requires.
Security
Adding sandbox permissions weakens the security boundary. Only add permissions you actually need.
const controller = mountHyperGen(el, {
bootstrapUrl: "/bootstrap",
extraSandboxPermissions: ["allow-popups"], // Use with caution
});See ADR-004 for the full security model rationale.