Skip to content
Public preview — you found hosted GGUI early · the console is open

Custom Theming

read as .md

GGUI theming has two independent layers, and they live on opposite sides of the iframe boundary. Keep them straight:

  1. Your host chrome — the React app around the render (the chat panel, the surrounding page). You theme this with @ggui-ai/design tokens: a <ThemeProvider> injects --ggui-* CSS variables on :root and your CSS references them.
  2. The generated UI inside the iframe — the component the agent rendered. You never style this from the host (it’s sandboxed on a different origin). Its palette comes from the operator’s ggui.json theme preset, resolved per render and applied as a runtime CSS-variable overlay.

The two are matched by convention: the ggui-basic-web sample picks the same preset for its host chrome that the demo ggui.json picks for the iframe, so the chat shell and the generated card share one palette.

For the full token reference (palettes, scales, semantic colors), see Design Tokens.

Layer 1 — host chrome via <ThemeProvider>

Section titled “Layer 1 — host chrome via <ThemeProvider>”

<ThemeProvider> (from @ggui-ai/design/themes) takes a raw DTCG token tree and a color mode, injects every token as a --ggui-* CSS variable on :root, and wires the base html, body font + surface colors. Pull a registered preset with getRawTheme(id, mode):

import { ThemeProvider, getRawTheme } from "@ggui-ai/design/themes";
// Resolve the raw DtcgTheme token tree for a registered preset.
const INDIGO_DARK = getRawTheme("indigo", "dark");
function App() {
return (
<ThemeProvider theme={INDIGO_DARK} mode="dark">
{/* Your chat panel / page chrome. Every CSS rule that references
a --ggui-* variable now resolves against the indigo/dark tokens. */}
</ThemeProvider>
);
}

Your stylesheet then references the injected variables — the shape the sample’s globals.css uses to map semantic chat-shell roles onto GGUI tokens. Always supply the hardcoded fallback, as the sample does: it is what paints when no theme has been injected yet.

:root {
--bg-app: var(--ggui-color-ground, #ffffff);
--bg-chat: var(--ggui-color-sunken, #f3f4f6);
--accent: var(--ggui-color-primary-500, #0ea5e9);
--fg: var(--ggui-color-onGround, #111827);
--fg-muted: var(--ggui-color-onSunken, #6b7280);
}
.chat {
background: var(--bg-chat);
color: var(--fg);
font-family: var(--ggui-font-family-sans);
}

@ggui-ai/design/themes ships a registry — list what’s available at runtime (e.g. to build a theme picker):

import { getThemeIds, listThemes, getDefaultThemeId } from "@ggui-ai/design/themes";
getThemeIds(); // ['ggui', 'indigo', 'claudic', …] — every registered id
listThemes(); // [{ id, name, description, modes }, …] — metadata for a picker
getDefaultThemeId(); // 'ggui'

Layer 2 — generated iframe content via ggui.json

Section titled “Layer 2 — generated iframe content via ggui.json”

You can’t reach into the sandboxed iframe with CSS. Instead, the operator’s ggui.json theme block sets the app’s default preset. At each render the server resolves the theme (per-render themeId override → App default → server fallback), snapshots it onto the GguiSession, and the iframe-runtime applies it as a --ggui-* CSS-variable overlay on the iframe’s :root — generated components reference the same token variables your host chrome does, so the palette is a runtime overlay, not generated code:

{
"schema": "1",
"protocol": "draft-2026-09-10",
"app": { "slug": "my-app", "name": "My App" },
"theme": {
"preset": "indigo",
"mode": "dark"
}
}

theme also accepts a bare string preset shorthand ("theme": "indigo"), an overrides map of flat dot-path token tweaks on top of a preset ({ "preset": "indigo", "mode": "dark", "overrides": { "color.primary.500": "#8b5cf6" } } — no custom theme file needed), and a { "file": "./theme.json", "mode": "dark" } form pointing at a DTCG theme file. Lint a theme file before serving with ggui theme validate <path>.

Match theme.preset / theme.mode here to the getRawTheme(...) you pass to <ThemeProvider> in your host, and the chat shell and the rendered card render in one cohesive palette — the sample does exactly this.

The preset isn’t fixed per app. Agents can list the available presets with the ggui_list_themes tool (each entry is { id, name, description, modes }) and pick one for a single render via the themeId input on ggui_render — the override wins over the app default for that render only. App.availableThemeIds allowlists which presets agents may pick.

The token layer is just CSS custom properties with hardcoded fallbacks, so you can also override individual variables without a registered preset — useful for a quick rebrand on top of a chosen theme, or for scoping one subtree.

Set tokens on :root to restyle every host-chrome surface in the page:

:root {
/* Primary brand color (50 → 900 scale) */
--ggui-color-primary-50: #eff6ff;
--ggui-color-primary-500: #3b82f6;
--ggui-color-primary-600: #2563eb;
--ggui-color-primary-900: #1e3a8a;
/* Typography */
--ggui-font-family-sans: "Inter", system-ui, sans-serif;
--ggui-font-family-mono: "JetBrains Mono", ui-monospace, monospace;
/* Shape + density */
--ggui-spacing-md: 16px;
--ggui-shape-radius-md: 8px;
--ggui-shape-shadow-md: 0 8px 16px -4px rgba(15, 23, 42, 0.1);
}

These run on top of whatever <ThemeProvider> injected — the provider writes the :root block first, your stylesheet’s later :root rules win on equal specificity.

CSS custom properties cascade — wrap any subtree to give it its own accent without leaking to siblings:

<section
style={
{
"--ggui-color-primary-600": "#8b5cf6",
"--ggui-shape-radius-md": "1rem",
} as React.CSSProperties
}
>
{/* Everything in here picks up the purple primary + softer corners */}
</section>
Category Pattern Example
Colors (primary) --ggui-color-primary-{50-900} --ggui-color-primary-600
Colors (neutral) --ggui-color-neutral-{50-900} --ggui-color-neutral-200
Colors (status) --ggui-color-{success,warning,error,info}-{50,100,200,500,600,700,800} --ggui-color-success-500
Layering roles --ggui-color-{ground,onGround,container,onContainer,elevated,onElevated,sunken,onSunken,outline,outlineVariant}ground is the canvas, container the card, sunken the well; elevated is derived --ggui-color-onGround
Role pairs --ggui-color-{onPrimary,primaryContainer,onPrimaryContainer,onError,errorContainer,onErrorContainer,tertiary,onTertiary,tertiaryContainer,onTertiaryContainer} --ggui-color-onPrimary
Typography --ggui-font-{family,size,weight,lineHeight}-* — sizes and line-heights come from the one font.ramp; a theme authors font.{family,weight,ramp,letterSpacing} only --ggui-font-size-lg
Spacing --ggui-spacing-{xs,sm,md,lg,xl,2xl,3xl} --ggui-spacing-md
Shape — radius --ggui-shape-radius-{none,sm,md,lg,xl,2xl,full} --ggui-shape-radius-lg
Shape — shadow --ggui-shape-shadow-{none,xs,sm,md,lg,xl,2xl} --ggui-shape-shadow-md
Motion — duration --ggui-motion-duration-{instant,fast,normal,slow,slower} --ggui-motion-duration-normal
Motion — transition --ggui-motion-transition-{fast,normal,slow,colors,opacity,transform} --ggui-motion-transition-colors
Accessibility --ggui-accessibility-focusRing-{color,width,offset}, --ggui-accessibility-reducedMotion-duration, --ggui-accessibility-highContrast-{borderWidth,textColor,backgroundColor,linkColor} --ggui-accessibility-focusRing-color
Z-Index --ggui-zIndex-{hide,base,docked,dropdown,sticky,banner,overlay,modal,popover,skipLink,toast,tooltip} --ggui-zIndex-modal

Status colors are ramps derived from one anchor — a theme authors a single 500 for each of success, warning, error and info, and the 50/100/200/500/600/700/800 stops, the inks, the containers and the outlines are derived from it. Layering roles are pairsground/onGround (the canvas and its text), container/onContainer (cards), sunken/onSunken (wells, inputs at rest, code blocks, secondary text) — with elevated/onElevated derived; use them to keep contrast right across nested backgrounds. A theme.json in the old shape (surface/surfaceVariant, authored font.size / font.lineHeight, motion.duration / motion.easing, $metadata.fontUrl) is refused: ggui serve refuses to start on it, and ggui theme validate <path> names the field.

See Design Tokens for live swatches and the complete scale.