Skip to content

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.

A host supplies @font-face descriptors on the theme, as SynapseTheme.fontFaces. They travel over the wire as the synapse/fontFaces host-context extension, and the SDK loads them into the app document.

// Host side
{
mode: "dark",
tokens: { "--font-sans": "'Your Sans', system-ui, sans-serif" },
fontFaces: [
{
family: "Your Sans",
src: "url('/fonts/your-sans.woff2') format('woff2')",
weight: "400 700",
},
],
}
Field Notes
family Must match the family named in the host’s --font-* token value.
src Any CSS src descriptor — relative, absolute, or data:.
weight Single weight (400) or a variable range (400 700). Optional.
style normal, italic, … Optional.
display Defaults to swap, so text paints in the fallback rather than blocking. Optional.

Faces are loaded through the CSS Font Loading API (new FontFace(...)), not by building @font-face CSS text. Family and src stay separate arguments, so a host value can never escape a rule and inject styles into the app; the browser also parses src against its own grammar, so relative paths, absolute URLs, and data: URIs behave identically.

Sending no fonts is supported, not degraded

Section titled “Sending no fonts is supported, not degraded”

Omit fontFaces 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.

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.

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.

A host-context-changed notification carries only the fields that changed, so an absent synapse/fontFaces means “unchanged” and loaded faces stay loaded — a dark-mode toggle never drops the app’s typeface. A host clears faces by sending an explicit empty list.

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.