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 @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.
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”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.
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.