The typeface never lived in the token catalog
A gallery widget can copy every brand token and every utility class and still render in Times. The typeface was never in those catalogs. On a Next.js host, next/font mints a name like --font-geist-sans and puts Geist on body. A class named font-sans is only a pointer at that name. The widget that lands on a page that never ran that loader does not get the face.
CSS then makes it worse. If font-family asks for var(--font-geist-sans) and that name is missing, the browser drops the entire declaration. It does not walk the comma list to sans-serif. The fallback is Times.
Tokens cannot fetch a face. Class bodies wrap as a descendant rule, so a pasted @font-face is ignored. Project fonts are the third catalog: RuleCMS loads the face and, when you ask, sets font-family on the widget so Preview and the published embed match.
Where this is really useful
| You want | Without project fonts | With this catalog |
|---|---|---|
A widget that matches a Tailwind / next/font gallery on a host that did not load Geist | Tokens point at --font-geist-sans. The name is missing. Type falls back to Times. | RuleCMS registers Geist and defines the token on the widget. The embed matches Preview. |
| A system stack only — no network, never Geist | The widget inherits whatever the host body uses, or Times if that inheritance never arrives. | Choose stack-only. The browser uses faces it already has. Nothing downloads. |
| A Google family with specific weights and subsets | Someone pastes a gstatic URL that goes stale, or the face never loads on a partner page. | You pick the family, weights, and subsets. RuleCMS builds the Google Fonts CSS URL. Authors never paste a file path. |
A host that already runs next/font (or ships the same faces) | Works today. Do not double-load the face. | Turn published inclusion off. Composer and Preview still use the catalog so authors see the typeface while they work. |
When to use it / when not to
Use project fonts when RuleCMS must own type on every embed host — a landing page, a partner site, or any route that did not run your font loader. Fill the catalog once. Composer, Preview, and — by default — published widgets then show the same face.
Skip the catalog, or turn published inclusion off, when the host stylesheet already registers the face and sets font-family on the widget's ancestors. Pointers in tokens and classes still work. RuleCMS just does not ship a second copy of the files.
The project catalog
Open an environment, then the Fonts card. That page is the catalog for the project, not for one environment: Development, Staging, and Production share it. View opens the preview (the default). Edit is /fonts/edit.
- Preview shows the CSS this project will emit — a Google Fonts stylesheet link when a face is loaded, plus the token and widget
font-familyon the render surface. That listing is for authors. Published widgets do not dump a:rootrule onto your page. - Edit is a card per family. Invalid fields are refused per row. Catalog Save is explicit. You need permission to manage project CSS settings; viewing is a separate permission.
A project has no fonts row until someone saves for the first time. Until then, widgets rely entirely on the host typeface — the Times path if that host never registered the face.
What each field means
| Field | What it is |
|---|---|
| Family | The face name — Geist, Inter. Not a CSS file. For Google, you pick from the allowlist. |
| CSS variable (optional) | The token this face defines, for example --font-geist-sans. Utilities and class bodies may keep pointing at it. A token that says var(--font-geist-sans) without this entry is the Times bug. |
| Fallbacks | Segoe UI, system-ui, sans-serif. Comma-separated family names. RuleCMS builds the file URL; you pick the family — you never paste a network path here. |
| Source: stack-only | Use faces the browser already has. No download. That matches “looks like a modern sans site.” It will never be Geist. |
| Source: Google | An allowlisted family plus weights and subsets. RuleCMS writes the Google Fonts CSS URL. You never paste a gstatic path. |
| Use as the widget's default typeface | At most one row. Sets font-family on the widget's first real children. Other rows only mint a token and load the face, so a class or leaf can point at them. |
Published widgets
On the project's Fonts page, the Published widgets accordion at the top holds two radios. Changing a radio saves immediately. Catalog Save is separate. The first is selected by default:
- Include these fonts when published widgets are rendered. A non-empty catalog is sent with the widget and the renderer loads the faces inside the widget. This is how RuleCMS makes Preview and the live embed match.
- My apps already load these fonts. Do not include them again when the widget is being rendered. Published widgets still point at
--font-geist-sans, but RuleCMS does not ship the files. Your host must register the face. Use this when the app already runsnext/font.
var() on the live page drops the whole font-family — Times again. Check the published widget on a real route after you flip the setting.What the renderer does
| Surface | What happens |
|---|---|
| Composer canvas and Preview | Always injects when the catalog is non-empty. The published-inclusion flag is ignored here so authors still see the typeface while they work. |
| Published widget (React SDK and HTML custom element) | The widget fetch includes the catalog when inclusion is on. The HTML custom element ships the same React bundle, so the emit is the same. |
You do not republish a widget to change a face. The catalog is joined when the host fetches the widget. The next uncached fetch picks up the current families. (Published traffic may sit in widget-cache for a short TTL — the same delay itemList already has.)
Hosts already fetch the widget once. RuleCMS hangs fonts on that same response as a sibling of the widget, its collections, CSS variables, and class definitions — not folded into itemList.
{
"success": true,
"data": {
"widget": { "publishedKey": "…", "itemList": "…", "…": "…" },
"childCollections": { },
"fontFaces": [
{
"family": "Geist",
"cssVariable": "--font-geist-sans",
"fallbacks": ["Segoe UI", "system-ui", "sans-serif"],
"applyToWidget": true,
"source": { "kind": "google", "weights": [400, 700], "subsets": ["latin"] }
}
]
}
}fontFaces is omitted when the catalog is empty, when inclusion is off, or if the catalog cannot be read. When inclusion is off, the same response also sends includeFontFaces: false so renderers that still receive a list must not emit it. The widget still loads.
The React renderer (@rulecms/widget-react) and the HTML custom element turn a non-empty catalog into a Google stylesheet link when a face is loaded, plus one scoped <style> on the widget's first real children. @font-face (when emitted) is unscoped — a face has to register on the document. The token and font-family land on [data-rulecms-published-key] > *, not on :root:
[data-rulecms-published-key="env---widget-abc"] > * {
--font-geist-sans: Geist, "Segoe UI", system-ui, sans-serif;
font-family: var(--font-geist-sans);
}The Development draft fetch (Development Integration) does the same join, so a dev. token preview on your site matches composer.
Good to know
- Light DOM does not mean the host
bodywins. Once RuleCMS setsfont-familyon the widget's children, host type no longer inherits into that tree. Turn inclusion off if you want the host face to win. - One catalog per project. There is not a separate list per environment. Promote and publish the widgets; the faces stay project-wide.
- No republish to restyle a face. Change the catalog. The next uncached widget fetch picks it up.
- The composer font-family picker is a different tool. That drawer control is an allowlisted stack (system faces). It does not load Geist. This catalog is who defines
--font-geist-sans. - React and the HTML embed, this pass. Other framework SDKs are not in this release.
- Keep the list small. At most 12 fonts and 8 fallbacks per face. Google families, weights, and subsets come from an allowlist — RuleCMS builds the file URL; you pick the family.
Related docs
- CSS Variables — the token catalog; a pointer at
--font-geist-sansis not a face - Class Definitions — project class-name → declaration-block catalog; independent inclusion toggle
- Keyframes — named
@keyframesso copied animate utilities have frames to run - Themes — host-dark / color-scheme token overrides
- Projects, Environments & Tokens
- The Composer
- HTML Custom Element Embed
- Development Integration
- API Reference — the public widget GET, including
fontFaces