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-.
How it works
Section titled “How it works”When your app’s HTML loads in an iframe, the platform:
- 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. - Sends
ui/initializevia postMessage with the same tokens inparams.theme.tokens(the NimbleBrain legacy notification path). - Sends
ui/notifications/host-context-changedwhen the user toggles dark mode, with updated token values understyles.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.
Using tokens in CSS
Section titled “Using tokens in CSS”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);}Running outside the platform
Section titled “Running outside the platform”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.
Handling dark mode
Section titled “Handling dark mode”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.
Token reference
Section titled “Token reference”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.
Colors (ext-apps spec)
Section titled “Colors (ext-apps spec)”| Token | Use for |
|---|---|
--color-background-primary | Page background |
--color-background-secondary | Card / panel backgrounds |
--color-background-tertiary | Subtle / nested backgrounds |
--color-text-primary | Body text |
--color-text-secondary | Captions, metadata, placeholders |
--color-text-tertiary | De-emphasized text |
--color-text-accent | Links, primary buttons, AI accents (load-only — does not refresh on toggle) |
--color-border-primary | Borders, dividers |
--color-border-secondary | Secondary borders |
--color-ring-primary | Focus rings |
Typography (ext-apps spec)
Section titled “Typography (ext-apps spec)”| Token | Use for |
|---|---|
--font-sans | Body font |
--font-mono | Code / monospace font |
--font-weight-normal | Normal weight |
--font-weight-medium | Medium weight |
--font-weight-semibold | Semibold weight |
--font-weight-bold | Bold weight |
--font-text-3xs-size / -line-height | Dense metadata, badge text |
--font-text-2xs-size / -line-height | Captions, timestamps |
--font-text-xs-size / -line-height | Extra-small body text |
--font-text-sm-size / -line-height | Small body text |
--font-text-base-size / -line-height | Base body text |
--font-text-lg-size / -line-height | Large body text |
--font-heading-sm-size / -line-height | Small heading |
--font-heading-md-size / -line-height | Medium heading |
--font-heading-lg-size / -line-height | Large heading |
Layout (ext-apps spec)
Section titled “Layout (ext-apps spec)”| Token | Use for |
|---|---|
--border-radius-xs | Tight corners |
--border-radius-sm | Small corners |
--border-radius-md | Default corners |
--border-radius-lg | Large corners |
--border-radius-xl | Extra-large corners |
--border-width-regular | Standard border width |
Effects (ext-apps spec)
Section titled “Effects (ext-apps spec)”| Token | Use for |
|---|---|
--shadow-hairline | Hairline outline |
--shadow-sm | Small elevation |
--shadow-md | Medium elevation |
--shadow-lg | Large elevation |
NimbleBrain extensions (--nb-*)
Section titled “NimbleBrain extensions (--nb-*)”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).
| Token | Use for |
|---|---|
--nb-color-accent-foreground | Text on accent backgrounds |
--nb-color-danger | Errors, destructive actions |
--nb-color-danger-foreground | Text on danger backgrounds |
--nb-color-success | Success indicators |
--nb-color-warning | Warning indicators |
--nb-color-processing | In-progress / processing state |
--nb-color-processing-light | Processing background |
--nb-color-info-light | Info background |
--nb-font-heading | Heading font |
Opt-in, not opt-out
Section titled “Opt-in, not opt-out”Theme injection is non-breaking. The platform injects tokens into every iframe, but they have zero effect unless your CSS references them:
| Your CSS | What happens |
|---|---|
background: white | Stays 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.
Injected style cascade
Section titled “Injected style cascade”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
:rootand 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.
Debugging
Section titled “Debugging”If your tokens aren’t applying, open DevTools and inspect the iframe:
- In Chrome DevTools, open the Elements panel.
- Expand the iframe’s document (click the
#documentnode inside the<iframe>). - Select
<html>and check the Computed tab for--color-background-primary.
Common issues:
| Symptom | Cause | Fix |
|---|---|---|
| Background is not what I expected | Your CSS uses background: #fff instead of var(--color-background-primary) | Replace hardcoded colors with var() references |
| Tokens exist but aren’t applied | CSS specificity — a more specific selector overrides the token | Check that your selector isn’t more specific than the :root declaration |
| Dark mode doesn’t toggle | Missing ui/notifications/host-context-changed handler | Add 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 payload | Drive light/dark-sensitive surfaces from the spec --color-* tokens |
| Tokens are stale after restart | deps/ directory contains an old bundled copy | Delete deps/<your-package> during local development (see Local Development) |
Full example
Section titled “Full example”See the Hello World walkthrough for a complete app with theme integration, including both Python and TypeScript implementations.