HyperGen

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

  1. The host application defines a set of --hg-* CSS variables
  2. mountHyperGen() sends them to the iframe as a hg:theme postMessage
  3. The iframe's bridge script applies them to :root
  4. Agent-generated HTML uses var(--hg-*) in its CSS
  5. 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

VariablePurposeDefault
--hg-surfacePrimary background surface#ffffff
--hg-surface-elevatedElevated surfaces (cards, popovers)#f9fafb
--hg-textPrimary text color#111827
--hg-text-mutedSecondary / muted text#6b7280
--hg-accentAccent / brand color (buttons, links)#7c3aed
--hg-accent-fgForeground color on accent backgrounds#ffffff
--hg-borderBorders and separators#e5e7eb

Typography

VariablePurposeDefault
--hg-font-familyBase font familysystem-ui, -apple-system, sans-serif
--hg-font-monoMonospace font familyui-monospace, monospace
--hg-font-sizeBase font size16px
--hg-line-heightBase line height1.5

Spacing

VariablePurposeDefault
--hg-space-1Extra-small spacing4px
--hg-space-2Small spacing8px
--hg-space-4Medium spacing16px
--hg-space-8Large spacing32px

Shape

VariablePurposeDefault
--hg-radiusDefault border radius8px
--hg-radius-smSmall border radius4px
--hg-radius-lgLarge border radius12px

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>

On this page