/docsfor

Theming

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

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

VariableUse
--color-backgroundthe host page behind the widget; for knockouts only
--color-surfacecards, panels, popovers
--color-surface-mutedwells, table stripes, hover fills, code blocks
--color-textbody text, headings, primary values
--color-text-mutedlabels, secondary text, axis titles
--color-text-subtlecaptions, tick labels, placeholders, disabled
--color-borderhairlines, dividers, card outlines, gridlines
--color-border-stronginputs, emphasized outlines, focus-adjacent edges
--color-accentprimary actions, selection, the one highlighted series
--color-accent-texttext and icons placed on --color-accent
--color-dangererrors, destructive actions, negative deltas
--color-successsuccess, positive deltas
--color-warningwarnings, pending states
--color-infoneutral notices, links inside callouts
--chart-1 to --chart-6categorical series, in order
--font-sansall UI text
--font-monocode, tabular identifiers
--radius-sm, --radius-md, --radius-lgchips 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:

TokenRead 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-subtlethe 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 variableFrom
--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());