# Theming
URL: /generative-frame/docs/theming

How the host page's theme becomes CSS variables in the frame, under canonical and MCP Apps names, and stays in sync.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

A widget is styled with CSS variables that the host sends into the frame. A widget that uses only these variables matches the host in light and dark mode, and changes with it live.

## Tokens

```
type ThemeTokens = {
  colorScheme: "light" | "dark";
  variables: Record<string, string>;
};
```

`variables` is keyed by CSS custom property name. The frame applies them on `:root`, sets `color-scheme` and `data-theme` on the root element, and themes plain elements (headings, links, tables, buttons, inputs) with them before any widget CSS loads. The widget's background stays transparent.

## Variables a widget can use

| Variable                                    | Use                                                    |
| ------------------------------------------- | ------------------------------------------------------ |
| `--color-background`                        | the host page behind the widget; for knockouts only    |
| `--color-surface`                           | cards, panels, popovers                                |
| `--color-surface-muted`                     | wells, table stripes, hover fills, code blocks         |
| `--color-text`                              | body text, headings, primary values                    |
| `--color-text-muted`                        | labels, secondary text, axis titles                    |
| `--color-text-subtle`                       | captions, tick labels, placeholders, disabled          |
| `--color-border`                            | hairlines, dividers, card outlines, gridlines          |
| `--color-border-strong`                     | inputs, emphasized outlines, focus-adjacent edges      |
| `--color-accent`                            | primary actions, selection, the one highlighted series |
| `--color-accent-text`                       | text and icons placed on `--color-accent`              |
| `--color-danger`                            | errors, destructive actions, negative deltas           |
| `--color-success`                           | success, positive deltas                               |
| `--color-warning`                           | warnings, pending states                               |
| `--color-info`                              | neutral notices, links inside callouts                 |
| `--chart-1` to `--chart-6`                  | categorical series, in order                           |
| `--font-sans`                               | all UI text                                            |
| `--font-mono`                               | code, tabular identifiers                              |
| `--radius-sm`, `--radius-md`, `--radius-lg` | chips and inputs; buttons and cards; large panels      |

`buildWidgetGuidance` writes this list, with a usage note per token, into the model's guidance. Variables you add beyond these are listed to the model too when you pass your `tokens` to the guidance.

`DEFAULT_LIGHT_TOKENS` and `DEFAULT_DARK_TOKENS` (or `defaultThemeTokens(scheme)`) are the built-in palettes. A frame created without `tokens` uses the light one.

## Read the page theme

```
import { readThemeTokens } from "generative-frame";

const tokens = readThemeTokens(); // from document.documentElement
const panelTokens = readThemeTokens(panelElement);
```

`readThemeTokens(element, sources)` reads the computed custom properties of `element` and detects its color scheme. Tokens the page does not define fall back to the built-in palette for that scheme.

The color scheme comes from the nearest `.dark`, `.light`, `[data-theme]`, or `[data-mode]` ancestor, then the element's `color-scheme`, then the luminance of its background, then `prefers-color-scheme`.

### Sources and shadcn/ui

By default each token is read from its canonical name first and then from the matching shadcn/ui variable:

| Token                                       | Read from                                         |
| ------------------------------------------- | ------------------------------------------------- |
| `--color-background`                        | `--color-background`, `--background`              |
| `--color-surface`                           | `--color-surface`, `--card`, `--background`       |
| `--color-surface-muted`                     | `--color-surface-muted`, `--muted`, `--secondary` |
| `--color-text`                              | `--color-text`, `--foreground`                    |
| `--color-text-muted`, `--color-text-subtle` | the canonical name, `--muted-foreground`          |
| `--color-border`                            | `--color-border`, `--border`                      |
| `--color-border-strong`                     | `--color-border-strong`, `--input`, `--ring`      |
| `--color-accent`                            | `--color-accent`, `--primary`                     |
| `--color-accent-text`                       | `--color-accent-text`, `--primary-foreground`     |
| `--color-danger`                            | `--color-danger`, `--destructive`                 |
| `--radius-md`                               | `--radius-md`, `--radius`                         |

The other tokens are read from their canonical names only. Older shadcn themes that store bare HSL channels (`222 47% 11%`) are wrapped in `hsl()`. When the page defines `--radius` but not `--radius-sm` or `--radius-lg`, those are derived from it, and `--font-sans` falls back to the body's computed font family.

Override the lookup per token with `ThemeTokenSources`:

```
const tokens = readThemeTokens(document.documentElement, {
  "--color-accent": ["--brand", "--primary"],
  "--chart-1": "--brand-chart-1",
});
```

Map only design tokens. Every value you map is sent into the frame, where model-written or third-party widget code can read it.

Tailwind v4 aliases `--color-accent` to shadcn's muted `--accent`, so in a Tailwind v4 app the canonical name holds the wrong color. `useAssistantUiThemeTokens` from `generative-frame/assistant-ui` reads the shadcn variables first.

## MCP Apps names

Each frame also receives the MCP Apps standard style variables, so widgets written for MCP Apps hosts pick up the theme. Examples:

| MCP Apps variable              | From                 |
| ------------------------------ | -------------------- |
| `--color-background-primary`   | `--color-background` |
| `--color-background-secondary` | `--color-surface`    |
| `--color-text-primary`         | `--color-text`       |
| `--color-text-secondary`       | `--color-text-muted` |
| `--color-border-primary`       | `--color-border`     |
| `--color-ring-primary`         | `--color-accent`     |
| `--border-radius-md`           | `--radius-md`        |

The status backgrounds (`--color-background-danger` and the others) are the status colors at 12% opacity. Font weights, font sizes, `--border-radius-full`, and `--border-width-regular` are fixed values. The variables also reach MCP Apps widgets through the host context (`styles.variables`).

## Change the theme

```
widget.setTheme(readThemeTokens());
```

`setTheme` updates the variables in the frame without reloading it. In React, a new `tokens` prop does the same, and `useThemeTokens` produces new tokens when the page's theme changes.

`useThemeTokens(element?, sources?, options?)` re-reads when `class`, `data-theme`, `data-mode`, or `style` changes on `<html>` (or on `element`), when `class`, `data-theme`, or `data-mode` changes on `<body>`, and when the system color scheme flips. Every component that asks for the same element and sources shares one observer, and a change costs one read per animation frame however many widgets are mounted. Body `style` is ignored by default because scroll-lock and dialog libraries rewrite it constantly; pass `{ observeBodyStyle: true }` if your app sets theme variables there.

Inside the frame, a theme change fires a `themechange` event on `window` with `{ theme, variables }` as its `detail`. Canvas-based libraries cannot read CSS variables, so a widget that draws on a canvas resolves them and redraws on that event:

```
const color = () =>
  getComputedStyle(document.documentElement).getPropertyValue("--chart-1").trim();

window.addEventListener("themechange", () => chart.update());
// or: genframe.on("themechange", () => chart.update());
```