Theming
Style HyperGen UIs with CSS custom properties. Light and dark themes, dynamic switching, and tips for agents generating themed HTML.
HyperGen uses CSS custom properties (variables) prefixed with --hg-* to bridge the host application's design system into the sandboxed iframe. The host sends theme variables to the iframe via postMessage, and agent-generated HTML references those variables for consistent styling.
How It Works
- The host application defines a set of
--hg-*CSS variables mountHyperGen()sends them to the iframe as ahg:themepostMessage- The iframe's bridge script applies them to
:root - Agent-generated HTML uses
var(--hg-*)in its CSS - When the host updates the theme, the iframe updates instantly
This means the agent does not need to know what colors or fonts the host uses. It generates HTML with var(--hg-accent) and the host decides what --hg-accent actually looks like.
CSS Variable Reference
Colors
| Variable | Purpose | Default |
|---|---|---|
--hg-surface | Primary background surface | #ffffff |
--hg-surface-elevated | Elevated surfaces (cards, popovers) | #f9fafb |
--hg-text | Primary text color | #111827 |
--hg-text-muted | Secondary / muted text | #6b7280 |
--hg-accent | Accent / brand color (buttons, links) | #7c3aed |
--hg-accent-fg | Foreground color on accent backgrounds | #ffffff |
--hg-border | Borders and separators | #e5e7eb |
Typography
| Variable | Purpose | Default |
|---|---|---|
--hg-font-family | Base font family | system-ui, -apple-system, sans-serif |
--hg-font-mono | Monospace font family | ui-monospace, monospace |
--hg-font-size | Base font size | 16px |
--hg-line-height | Base line height | 1.5 |
Spacing
| Variable | Purpose | Default |
|---|---|---|
--hg-space-1 | Extra-small spacing | 4px |
--hg-space-2 | Small spacing | 8px |
--hg-space-4 | Medium spacing | 16px |
--hg-space-8 | Large spacing | 32px |
Shape
| Variable | Purpose | Default |
|---|---|---|
--hg-radius | Default border radius | 8px |
--hg-radius-sm | Small border radius | 4px |
--hg-radius-lg | Large border radius | 12px |
Extensible by convention
These are the standard variables. Agents and hosts can agree on additional --hg-* variables (e.g., --hg-error, --hg-success, --hg-warning) as needed. The protocol does not enforce a closed set.
Light Theme Example
controller.setTheme({
"--hg-surface": "#ffffff",
"--hg-surface-elevated": "#f9fafb",
"--hg-text": "#111827",
"--hg-text-muted": "#6b7280",
"--hg-accent": "#7c3aed",
"--hg-accent-fg": "#ffffff",
"--hg-border": "#e5e7eb",
"--hg-font-family": "system-ui, -apple-system, sans-serif",
"--hg-font-mono": "ui-monospace, monospace",
"--hg-font-size": "16px",
"--hg-line-height": "1.5",
"--hg-space-1": "4px",
"--hg-space-2": "8px",
"--hg-space-4": "16px",
"--hg-space-8": "32px",
"--hg-radius": "8px",
"--hg-radius-sm": "4px",
"--hg-radius-lg": "12px",
});Dark Theme Example
controller.setTheme({
"--hg-surface": "#111827",
"--hg-surface-elevated": "#1f2937",
"--hg-text": "#f9fafb",
"--hg-text-muted": "#9ca3af",
"--hg-accent": "#818cf8",
"--hg-accent-fg": "#ffffff",
"--hg-border": "#374151",
"--hg-font-family": "system-ui, -apple-system, sans-serif",
"--hg-font-mono": "ui-monospace, monospace",
"--hg-font-size": "16px",
"--hg-line-height": "1.5",
"--hg-space-1": "4px",
"--hg-space-2": "8px",
"--hg-space-4": "16px",
"--hg-space-8": "32px",
"--hg-radius": "8px",
"--hg-radius-sm": "4px",
"--hg-radius-lg": "12px",
});Dynamic Theme Switching
Toggle between light and dark themes at runtime:
const themes = {
light: {
"--hg-surface": "#ffffff",
"--hg-surface-elevated": "#f9fafb",
"--hg-text": "#111827",
"--hg-text-muted": "#6b7280",
"--hg-accent": "#7c3aed",
"--hg-accent-fg": "#ffffff",
"--hg-border": "#e5e7eb",
},
dark: {
"--hg-surface": "#111827",
"--hg-surface-elevated": "#1f2937",
"--hg-text": "#f9fafb",
"--hg-text-muted": "#9ca3af",
"--hg-accent": "#818cf8",
"--hg-accent-fg": "#ffffff",
"--hg-border": "#374151",
},
};
let isDark = false;
document.getElementById("theme-toggle")!.addEventListener("click", () => {
isDark = !isDark;
controller.setTheme(isDark ? themes.dark : themes.light);
});The transition is instant -- setTheme() sends a postMessage and the iframe's bridge script updates :root styles immediately. If you want smooth transitions, add transition rules in the iframe's extraHead CSS:
bootstrapHtml({
sseEndpoint: "/api/stream",
extraHead: `<style>
body { transition: background 0.3s, color 0.3s; }
.card { transition: background 0.3s, border-color 0.3s; }
</style>`,
});Initial Theme via Server
You can set the initial theme on the server side using themeVars in bootstrapHtml(). This avoids a flash of unstyled content before the client-side setTheme() call:
const html = bootstrapHtml({
sseEndpoint: "/api/stream",
themeVars: {
"--hg-surface": "#111827",
"--hg-text": "#f9fafb",
"--hg-accent": "#818cf8",
},
});These variables are injected as CSS declarations in the bootstrap document's <style> block, so they are available before any JavaScript runs.
Using hypergen-theme.ts for Type Safety
The optional hypergen-theme.ts file provides TypeScript types and utilities for working with theme variables:
import { defaultTheme, serializeTheme, type HyperGenTheme } from "./hypergen-theme";
// defaultTheme has all standard variables with their default values
console.log(defaultTheme["--hg-accent"]); // "#7c3aed"
// Type-safe theme construction
const myTheme: Partial<HyperGenTheme> = {
"--hg-accent": "#10b981",
"--hg-surface": "#0f172a",
};
// Serialize to CSS string (useful for inline styles)
const css = serializeTheme(myTheme);
// "--hg-accent: #10b981; --hg-surface: #0f172a"The HyperGenTheme interface enforces that all keys start with --hg- and covers the full set of standard variables. Use Partial<HyperGenTheme> when you only want to override a subset.
Tips for Agents Generating Themed HTML
When building HTML that agents will generate, follow these guidelines:
Always use CSS variables for visual properties
<!-- Good: adapts to any theme -->
<div style="
background: var(--hg-surface-elevated);
color: var(--hg-text);
border: 1px solid var(--hg-border);
border-radius: var(--hg-radius);
padding: var(--hg-space-4);
">
Content here
</div>
<!-- Bad: hardcoded colors won't match the host theme -->
<div style="background: #f9fafb; color: #111827; border: 1px solid #e5e7eb;">
Content here
</div>Provide fallback values
CSS var() accepts a fallback as the second argument. Include reasonable defaults so the UI works even if the host does not send a theme:
<p style="color: var(--hg-text, #1a1a1a);">
This text has a fallback color.
</p>Use semantic variables, not raw colors
Prefer --hg-accent over --hg-primary-600. The semantic names work across light and dark themes without adjustment.
Keep agent-generated styles inline or in fragment <style> blocks
Each streamed fragment should be self-contained. Use inline style attributes or include a <style> block within the fragment:
<style>
.agent-card {
background: var(--hg-surface-elevated);
border: 1px solid var(--hg-border);
border-radius: var(--hg-radius);
padding: var(--hg-space-4);
}
.agent-card h3 { color: var(--hg-text); }
.agent-card p { color: var(--hg-text-muted); }
</style>
<div class="agent-card">
<h3>Result</h3>
<p>Generated by the agent</p>
</div>Client Setup
Mount HyperGen iframes in your frontend — React, Vue, Svelte, or vanilla JS. Manage themes, handle resize, and control the lifecycle.
Agent Integration
Connect any AI agent to HyperGen — Claude, OpenAI, Ollama, or custom systems. The pattern is always the same — agent generates HTML, server streams it via SSE.