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. 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 this catalog | 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 this catalog 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 it on this widget. Composer and Preview show the face; publish to send it with the embed.
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.
This widget's catalog
Open the widget, then CSS Settings, then Fonts. This catalog belongs to this widget.
To change the template for the next widget you create, edit the project catalog.
View opens the preview (the default). Edit is /fonts/edit.
- Preview shows the CSS this catalog 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 this widget; viewing is a separate permission.
A new widget starts as a copy of the project fonts catalog. Until someone saves faces here (or the project had none to copy), this widget relies 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. |
Color palettes
Color fonts (COLRv1) can ship more than one set of ink. The host registers those sets with @font-palette-values, then a leaf or class points at the name with font-palette. That at-rule is document-global — a class body cannot register it, and tokens reject @. Palettes are optional fields on the same font card, not a second catalog.
Add a palette when the face needs a brand set of override colors and the embed host will not register it. Leave palettes empty when the host already owns font-palette, or when the face is not a color font — stack-only rows accept them, but they usually do nothing useful.
| Field | What it is |
|---|---|
| Palette name | A dashed ident such as --brand. RuleCMS emits @font-palette-values --brand. At most four palettes per face. |
| Override colors | Index plus a color: hex, a named color, or var(--token). No url(. At most eight overrides per palette. |
| Use as the widget's default palette | At most one row in this catalog. Sets font-palette: --brand on the widget's first real children. Other palettes only register; a class can still point at them. |
The Fonts inclusion radios also control palettes. When published widgets exclude fonts, RuleCMS does not emit @font-palette-values either. There is no separate palettes toggle.
@font-palette-values --brand {
font-family: Geist;
override-colors: 0 #111111, 1 var(--brand-accent);
}Published widgets
On this widget's Fonts page, the Published widgets accordion holds two radios. These radios control whether this widget's published and dev. embed carry the definitions. Composer and Preview still use this widget's catalog when inclusion is off. Changing a radio saves immediately. Catalog Save is separate.
The first radio 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 — including any color palettes on those faces. 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
Composer and Preview read this widget's draft catalog. The published embed and the Development (dev.) draft GET read the published widget's catalog. Publish is how a draft catalog goes live.
A new widget starts as a copy of the project catalog. After that, edits here do not write back to the project, and later project edits do not come back here.
| Surface | What happens |
|---|---|
| Composer canvas and Preview | Always inject from this widget's draft catalog when it is non-empty. The published-inclusion flag is ignored here so authors still see the result while they work. |
| Published widget (React SDK and HTML custom element) | The widget fetch includes this widget's catalog when inclusion is on. Nested collections inherit. You do not get a copy per collection. |
When the widget catalog is applied, the HTML custom element ships the same React bundle, so the emit is the same.
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"] },
"palettes": [
{
"name": "--brand",
"overrideColors": [
{ "index": 0, "color": "#111111" },
{ "index": 1, "color": "var(--brand-accent)" }
]
}
]
}
]
}
}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) reads the same fields from the draft widget row, 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. - This catalog belongs to this widget. Copy and promote copy these columns as-is. The project catalog is only the starting snapshot.
- Publish to restyle the live embed. Composer and Preview update when you save this draft catalog.
- 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 - Position try — named
@position-tryso overlay classes that point at--fliphave a try set - Themes — host-dark / color-scheme token overrides
- Cascade layers — wrap class definitions in
@layerso they compete with host Tailwind - Widget chrome — the same apply-to-widget slot for color-scheme and selection
- CSS Properties — typed
@property; another unscoped at-rule - Projects, Environments & Tokens
- The Composer
- HTML Custom Element Embed
- Development Integration
- API Reference — the public widget GET, including
fontFaces