Fonts & base reset
The token contract can name a font family (--font-sans), but a name only
renders if the font files are loaded in the document — and each Synapse app
runs in its own iframe, a separate document that inherits no @font-face from
the host page. A host sending only tokens is therefore naming a typeface the app
has no way to render.
The library ships no fonts. Font tokens fall back to web-safe system stacks, so an app renders correctly with no host, no network, and no font files. A host that wants its own typeface sends the faces alongside the tokens.
Host fonts
Section titled “Host fonts”A host supplies its typeface as @font-face CSS in the host context, under the
MCP Apps spec’s styles.css.fonts. The SDK loads that CSS into the app document.
// Host side: the ui/initialize hostContext, or a host-context-changed{ theme: "dark", styles: { variables: { "--font-sans": "'Your Sans', system-ui, sans-serif" }, css: { fonts: "@font-face { font-family: 'Your Sans'; src: url('/fonts/your-sans.woff2') format('woff2'); font-weight: 400 700; font-display: swap; }", }, },}- The
font-familyin each rule must match the family the host names in its--font-*variable. connect()applies the CSS with the spec’s ownapplyHostFonts; the cross-host client injects it the same way. Both use one style element,__mcp-host-fonts.- The browser fetches a face only when the app’s CSS first uses it, so an unused
weight costs nothing. Set
font-display: swapin the rule so text paints in the fallback first.
Sending no fonts is supported, not degraded
Section titled “Sending no fonts is supported, not degraded”Send no styles.css.fonts and the app renders in the web-safe fallbacks the
token contract defines — system-ui for
fontSans, ui-monospace for fontMono, and the same system-ui stack for
fontHeading — one family, hierarchy from weight and size.
Give every font token a web-safe tail
Section titled “Give every font token a web-safe tail”Write "'Your Sans', system-ui, sans-serif", never "'Your Sans'". A bare
family name with no matching face falls through to the browser default rather
than to your intended stack.
CSP: 'self' may not mean what you expect
Section titled “CSP: 'self' may not mean what you expect”A host that mounts apps in a srcdoc iframe without allow-same-origin — the
usual choice, since granting it to third-party app HTML is a sandbox escape —
gives the frame an opaque origin. CSP 'self' then matches nothing. Under a
typical font-src 'self' data:, a data: URI is the only form that works
unconditionally; any http(s) URL, self-hosted or CDN, needs the host to add that
origin to the frame’s policy. Check the host’s CSP before choosing.
If a face is blocked, nothing breaks — the app falls back to the neutral system stack. It just looks unbranded.
Updates mid-session
Section titled “Updates mid-session”The font CSS is injected once and stays loaded. A later host-context-changed
without styles.css — a dark-mode toggle, say — leaves the typeface in place.
A host that changes typeface mid-session is not supported; send the fonts once,
in the handshake.
Base reset
Section titled “Base reset”Import @nimblebrain/synapse/ui/base for its side effect to establish the
root-height chain a full-pane app needs:
import "@nimblebrain/synapse/ui/base";It injects two rules: the height chain
html, body, #root { height: 100% }so a height: 100% app shell resolves against the iframe’s allocated pane, plus
body { margin: 0 }so the pane has no user-agent margin gap. A percentage chain, not a viewport unit, is the correct mechanism here: the app sizes to the pane the host gave it, not to the screen.
The module also exports the function directly:
import { injectBaseReset } from "@nimblebrain/synapse/ui/base";AppFrame calls injectBaseReset() automatically when it renders, so apps built
on it get the chain for free. Import the module explicitly in your entry when you
want the chain established before React mounts (which avoids a first-paint
layout jump) or when you render a full-pane root without AppFrame.