Skip to content

Theming

The NimbleBrain platform injects CSS custom properties into every app iframe so your UI can match the host shell — including dark mode — without shipping your own color palette. The tokens follow the MCP ext-apps specification, with a small set of NimbleBrain-specific extensions prefixed --nb-.

When your app’s HTML loads in an iframe, the platform:

  1. Injects a <style> block into your <head> with the full token set (ext-apps --color-* / --font-* / --border-radius-* / --shadow-* plus the --nb-* extensions) for the current theme. This happens before your HTML renders — no flash of wrong colors.
  2. Sends ui/initialize via postMessage with the same tokens in params.theme.tokens (the NimbleBrain legacy notification path).
  3. Sends ui/notifications/host-context-changed when the user toggles dark mode, with updated token values under styles.variables.

Your CSS references these variables with var(--token). Inside the platform the token always has a value — the <style> block lands before your HTML renders, and a srcdoc iframe inherits nothing else — so no fallback is needed. If your app also has to render where nothing injects tokens, see Running outside the platform below.

body {
font-family: var(--font-sans);
background: var(--color-background-primary);
color: var(--color-text-primary);
}
button {
background: var(--color-text-accent);
color: var(--nb-color-accent-foreground);
border-radius: var(--border-radius-md);
}
input {
border: 1px solid var(--color-border-primary);
background: var(--color-background-secondary);
color: var(--color-text-primary);
}
.muted-text {
color: var(--color-text-secondary);
}

If your app also has to render where nothing injects these tokens — Claude Desktop, another MCP host, or your own dev server — give var() a fallback that is your app’s own default, not a copy of the platform’s current palette:

/* Your neutral, chosen by you. */
body {
background: var(--color-background-primary, white);
color: var(--color-text-primary, black);
}

Pick the values you would ship if NimbleBrain did not exist. A fallback copied out of the token reference is wrong the moment the platform’s palette moves, and nothing tells you it happened.

An app that only ever runs inside the platform should use the bare form shown under Using tokens in CSS, so that a missing token fails visibly rather than resolving to a plausible wrong colour.

The injected <style> block gives you the right tokens at load time. But when the user toggles dark mode mid-session, you need a JavaScript handler to apply the updated values:

function applyTokens(tokens) {
if (!tokens || typeof tokens !== 'object') return;
for (const [key, value] of Object.entries(tokens)) {
document.documentElement.style.setProperty(key, value);
}
}
window.addEventListener('message', (event) => {
const msg = event.data;
if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
// Apply tokens on initial load (legacy notification path)
if (msg.method === 'ui/initialize' && msg.params?.theme?.tokens) {
applyTokens(msg.params.theme.tokens);
}
// Apply tokens when host context changes (ext-apps spec)
if (msg.method === 'ui/notifications/host-context-changed') {
const vars = msg.params?.styles?.variables;
if (vars) applyTokens(vars);
}
});

applyTokens sets CSS variables on document.documentElement, which overrides the injected <style> values. Because your CSS uses var() references, the UI updates instantly.

The platform injects the following tokens into every iframe. Those in the ext-apps spec’s variable enum also cross the protocol boundary and refresh live on theme changes; everything else — the --nb-* extensions, and NimbleBrain’s own additions to spec-shaped families — is injected at load only. See the caveat above.

TokenUse for
--color-background-primaryPage background
--color-background-secondaryCard / panel backgrounds
--color-background-tertiarySubtle / nested backgrounds
--color-text-primaryBody text
--color-text-secondaryCaptions, metadata, placeholders
--color-text-tertiaryDe-emphasized text
--color-text-accentLinks, primary buttons, AI accents (load-only — does not refresh on toggle)
--color-border-primaryBorders, dividers
--color-border-secondarySecondary borders
--color-ring-primaryFocus rings
TokenUse for
--font-sansBody font
--font-monoCode / monospace font
--font-weight-normalNormal weight
--font-weight-mediumMedium weight
--font-weight-semiboldSemibold weight
--font-weight-boldBold weight
--font-text-3xs-size / -line-heightDense metadata, badge text
--font-text-2xs-size / -line-heightCaptions, timestamps
--font-text-xs-size / -line-heightExtra-small body text
--font-text-sm-size / -line-heightSmall body text
--font-text-base-size / -line-heightBase body text
--font-text-lg-size / -line-heightLarge body text
--font-heading-sm-size / -line-heightSmall heading
--font-heading-md-size / -line-heightMedium heading
--font-heading-lg-size / -line-heightLarge heading
TokenUse for
--border-radius-xsTight corners
--border-radius-smSmall corners
--border-radius-mdDefault corners
--border-radius-lgLarge corners
--border-radius-xlExtra-large corners
--border-width-regularStandard border width
TokenUse for
--shadow-hairlineHairline outline
--shadow-smSmall elevation
--shadow-mdMedium elevation
--shadow-lgLarge elevation

These have no ext-apps spec equivalent. They are injected into the iframe’s <style> block at load but do not cross the protocol boundary, so they will not refresh on a live theme toggle (see the caveat above).

TokenUse for
--nb-color-accent-foregroundText on accent backgrounds
--nb-color-dangerErrors, destructive actions
--nb-color-danger-foregroundText on danger backgrounds
--nb-color-successSuccess indicators
--nb-color-warningWarning indicators
--nb-color-processingIn-progress / processing state
--nb-color-processing-lightProcessing background
--nb-color-info-lightInfo background
--nb-font-headingHeading font

Theme injection is non-breaking. The platform injects tokens into every iframe, but they have zero effect unless your CSS references them:

Your CSSWhat happens
background: whiteStays white. Injected tokens are ignored.
background: var(--my-bg)Uses your --my-bg variable. No collision.
background: var(--color-background-primary, white)Picks up the platform theme. Falls back to white outside the platform.

Existing apps require zero changes — theming is entirely opt-in.

The platform injects a <style> block as the first child of <head>, before your app’s own <style> tags. This means:

  • Platform tokens are declared on :root and are available to your CSS.
  • The injected block includes a minimal body reset (margin: 0, font-family: var(--font-sans), background: var(--color-background-primary), color: var(--color-text-primary)).
  • Your app’s <style> comes after and wins the CSS cascade for any conflicting declarations.

If you define your own body { background: ... }, it overrides the injected reset. This is by design — your app’s styles always win.

If your tokens aren’t applying, open DevTools and inspect the iframe:

  1. In Chrome DevTools, open the Elements panel.
  2. Expand the iframe’s document (click the #document node inside the <iframe>).
  3. Select <html> and check the Computed tab for --color-background-primary.

Common issues:

SymptomCauseFix
Background is not what I expectedYour CSS uses background: #fff instead of var(--color-background-primary)Replace hardcoded colors with var() references
Tokens exist but aren’t appliedCSS specificity — a more specific selector overrides the tokenCheck that your selector isn’t more specific than the :root declaration
Dark mode doesn’t toggleMissing ui/notifications/host-context-changed handlerAdd the applyTokens message listener (see code above)
An --nb-* color doesn’t flip on toggle--nb-* tokens are injected at load only, not in the live payloadDrive light/dark-sensitive surfaces from the spec --color-* tokens
Tokens are stale after restartdeps/ directory contains an old bundled copyDelete deps/<your-package> during local development (see Local Development)

See the Hello World walkthrough for a complete app with theme integration, including both Python and TypeScript implementations.