---
title: Custom Theming
description: Match ggui-generated UIs to your brand with two-layer theming — design-token CSS variables for your host chrome, plus the operator's ggui.json theme preset for the generated iframe content.
---

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`](https://github.com/ggui-ai/ggui/tree/main/samples/apps/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](/design/tokens/).

## Layer 1 — host chrome via `<ThemeProvider>`

`<ThemeProvider>` (from `@ggui-ai/design/themes`) takes a raw [DTCG](https://design-tokens.github.io/community-group/format/) 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)`:

```tsx
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 — exactly how the sample's `globals.css` maps semantic chat-shell roles onto ggui tokens:

```css
:root {
  --bg-app: var(--ggui-color-background);
  --bg-chat: var(--ggui-color-surface);
  --accent: var(--ggui-color-primary-500);
  --fg: var(--ggui-color-onSurface);
  --fg-muted: var(--ggui-color-onSurfaceVariant);
}

.chat {
  background: var(--bg-chat);
  color: var(--fg);
  font-family: var(--ggui-font-family-sans);
}
```

:::tip[Embedded contexts]
When ggui renders inside a host that draws its own chrome around your content (a claude.ai chat bubble, Claude Desktop), pass `transparent` so the body background drops out and the host surface shows through: `<ThemeProvider theme={…} mode="dark" transparent>`. Token-level `--ggui-color-surface` stays opaque, so cards and panels inside still paint their own surfaces.
:::

### Discovering presets

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

```tsx
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`

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:

```json
{
  "schema": "1",
  "protocol": "draft-2026-06-12",
  "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.

### Per-render theme override

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.

## Ad-hoc CSS token overrides

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.

### Global override

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

```css
: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.

### Scoped theming

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

```tsx
<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>
```

## Token reference

| 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`        |
| Surface + content   | `--ggui-color-{surface,onSurface,surfaceVariant,onSurfaceVariant,container,onContainer,outline,outlineVariant}` | `--ggui-color-onSurface`          |
| Typography          | `--ggui-font-{family,size,weight,lineHeight}-*`                                                                 | `--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-{colors,opacity,transform,all}`                                                       | `--ggui-motion-transition-colors` |
| Accessibility       | `--ggui-accessibility-focusRing-{color,width,offset}` (plus `reducedMotion`, `highContrast`)                    | `--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 scales** — `success`, `warning`, `error`, and `info` each ship the full 50/100/200/500/600/700/800 stops (no singletons). **Material role pairs** (`surface`/`onSurface`, `surfaceVariant`/`onSurfaceVariant`, `container`/`onContainer`, `outline`/`outlineVariant`) keep contrast right across nested surfaces — override the pair together, never one half.

See [Design Tokens](/design/tokens/) for live swatches and the complete scale.

## See also

- [Design Tokens](/design/tokens/) — full palette, scales, and live swatches
- [React SDK](/sdk/react/) — `<AppRenderer>` + `useMcpAppsChat`, the web render-hosting surface
- [`ggui-basic-web` sample](https://github.com/ggui-ai/ggui/tree/main/samples/apps/ggui-basic-web) — the runnable reference that matches host chrome to `ggui.json`