Contents

CSS Variables

This is this widget's live CSS variable catalog. Composer and Preview read these draft values. Publish to send them with the published (and dev.) embed. A new widget starts as a snapshot of the project catalog; edits here stay on this widget.

Design systems speak in names. RuleCMS does too

Every serious brand already has a vocabulary for how it looks: --brand-surface for the page background, --brand-space-md for a comfortable gap, --border-hairline for the thin rule that shows up on every card. Those names live in a stylesheet. Everything else on the site points at them. Rebrand the palette, or let a visitor switch to dark mode, and the whole product follows — because nothing stored a hex code of its own.

A visual CMS that only lets you type #f7f7f5 into a colour picker is a second copy of that system. It looks right the day you publish and wrong the day the brand moves. RuleCMS supports CSS variables so a widget can point at the same names your product already uses. Authors stay in the composer. Engineers keep one source of truth. The published page does not drift.

That support has two halves, and both matter:

  1. Point a setting at a name. Spacing, colour, border, shadow, type — almost every visual control can store --brand-surface instead of a typed value. The widget carries the name. The page, or RuleCMS, supplies what it means.
  2. Define those names on this widget. Composer and Preview show these draft values — not just store the pointers. Publish to send them with the published (and dev.) embed. You are not guessing what --brand-space-md will look like on the live site.

Together they give you a CMS that participates in the design system instead of photocopying it. Change --brand-surface here and this widget follows, without teaching marketers a hex code they will mistype. Edits here do not write back to the project.

Where this is really useful

You wantTyping a value into RuleCMSPointing at a CSS variable
A brand colour that marketing already owns in the product stylesheetA second hex in every widget. Rebrand means a scavenger hunt.One name. The catalog — or your site — is the only place the colour lives.
A spacing scale (xs / sm / md / lg) used everywhere else on the siteSomeone types 13px by eye. The widget never quite matches the product.--brand-space-md is the same step the rest of the app uses.
Light and dark (or seasonal) themesDuplicate widgets, or hope nobody notices the CMS block that stayed light.The widget asks for names. Your theme swaps what those names mean.
A whole border or shadow from the design system (1px solid #ddd, a drop shadow)Recreating the parts by hand, then watching them drift.One token for the whole property. Same value the rest of the site already ships.
Editors who need to see the result in the composer, not only on productionFine — the picker shows the colour. Tokens historically did not.Define the name in this widget’s catalog. The canvas and Preview resolve it.
You do not have to choose between RuleCMS and your stylesheet. Most teams keep the same names in both places. RuleCMS includes this widget's catalog on the published embed by default so the embed looks correct even on a page that has not loaded your tokens yet. If that double definition is a problem, turn inclusion off — the setting is described below.

Two places a name can get its value

When a setting stores --brand-surface, the browser looks up that custom property at paint time. Someone has to define it. In RuleCMS there are two legitimate owners:

OwnerWhere you edit itWhen the widget sees it
Your site — the original design-system modelYour application stylesheetWhenever the host page already defines the name. RuleCMS never had to know the value.
This widget's catalogManage widget → CSS Settings CSS VariablesComposer, Preview, and — when inclusion is on — this widget's published embed.

The widget JSON never stores the hex. It stores the name. Definitions live on this widget, or on the host, or both. Changing a token here restyles this widget; it does not write back to the project.

Pointing a setting at a name

In the composer, open a component and look at any size, colour, or style control in the Modify drawer. Above the input is a Value / CSS variable toggle.

  1. Click CSS variable. The number, colour picker, or dropdown is replaced by a single text box.
  2. Type the name exactly as it should appear in CSS, starting with two hyphens — for example --brand-space-md. The help text under the box confirms what will be published: var(--brand-space-md).
  3. If that name is in this widget's catalog, the canvas and Preview update. If it only exists on your live site, you will see it there.

Border and Box Shadow can take a name for the whole property (a design-system token like --border-hairline) or for individual parts. The full control-by-control guide is Using a CSS variable instead of a typed value.

The name is the contract. RuleCMS checks the shape — two hyphens, then letters, numbers, hyphens, and underscores, up to 200 characters. It does not invent a fallback if the name is missing from both the catalog and the host page. A typo looks like an unset property: nothing paints, with no error banner.

This widget's catalog

Open the widget, then CSS Settings, then CSS Variables. This catalog belongs to this widget.

To change the template for the next widget you create, edit the project catalog.

Style settings on this widget can point at the names you define here.

  • Preview shows the catalog as a readable :root { … } listing so you can scan it as CSS. That listing is for authors. It is not how published widgets inject tokens on your site (a :root rule on the host page would leak into the rest of your layout).
  • Edit is a name-and-value form. Invalid names and values are refused per row. Save stays disabled until the catalog is clean. You need permission to manage this widget; viewing is a separate permission.

Each entry is one custom property:

--brand-surface: #f7f7f5;
--brand-space-md: 16px;
--border-hairline: 1px solid #ddd;

Values are a single CSS value, not a stylesheet. Nested var(--other) is allowed, so a token can be built from other tokens. Semicolons, braces, comments, and url( are not. At most 200 names. An empty catalog is a valid state — the project simply has no RuleCMS-owned tokens yet.

A project has no catalog row until someone saves variables for the first time. Until then, widgets that point at names rely entirely on the host stylesheet, the same as they always have.

What the renderer does with the catalog

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.

SurfaceWhat happens
Composer canvas and PreviewAlways 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.

If the catalog is empty, nothing extra is emitted. Host tokens still work. Widgets that never used CSS variables are unchanged.

A column that points at --brand-surface paints that colour on the canvas while you edit. Publish is how those draft values reach the live embed. (Published traffic may sit in widget-cache for a short TTL — the same delay itemList already has.)

Your site probably defines them too — and that is fine

The original token model is still the right one for a product that already has a design system: :root or a theme class on the host page defines --brand-surface, and RuleCMS widgets inherit it. The HTML custom element mounts in light DOM for exactly that reason — your variables and fonts still reach the widget tree.

Teams that also fill the RuleCMS catalog usually paste the same names. By default RuleCMS still includes those definitions on the published widget. Inside that widget, the RuleCMS values win over inherited host tokens, because they are specified closer to the content. The rest of your page is untouched: injection is scoped to the widget, not dumped on :root.

That default is the useful one when:

  • The widget is on a page that does not load your design-system stylesheet (a landing page, a partner site, an email-adjacent HTML shell).
  • Editors need the published embed to match Preview even if the host forgot a token.
  • You are still rolling tokens out on the host and the catalog is ahead of the stylesheet.

It is the wrong default when the host stylesheet is the only source of truth you want at runtime — especially if RuleCMS and the host might disagree for a while.

Turn off inclusion on published widgets

On this widget's CSS Variables 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 CSS variables when published widgets are rendered. A non-empty catalog is sent with the widget and the renderer defines the names inside the widget. This is how RuleCMS makes Preview and the live embed match.
  • My apps already have the CSS variables. Do not include them again when the widget is being rendered. Published widgets still point at --brand-surface, but RuleCMS does not ship the definitions. Your client project must define the names. Use this to avoid two copies of the same payload: one in your stylesheet, one inside the widget.
Composer and Preview still use this widget's catalog when inclusion is off. Authors keep seeing tokens while they work. Only the published (and Development-draft) embed on your site stops carrying the extra definitions. Empty catalog plus the setting on is the same as off: there is nothing to include.
If you turn inclusion off, the host must define every name the widgets use. A missing token on the live page paints as nothing — the same as a typo. Check the published widget on a real route after you flip the setting.

How a published widget receives the catalog

Hosts already fetch the widget once. RuleCMS hangs the catalog on that same response as a sibling of the widget and its collections — not folded into itemList, not copied onto each collection.

{
  "success": true,
  "data": {
    "widget": { "publishedKey": "…", "itemList": "…", "…": "…" },
    "childCollections": { },
    "cssVariables": {
      "--brand-surface": "#f7f7f5",
      "--brand-space-md": "16px"
    }
  }
}

cssVariables 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 includeCssVariables: false so renderers that still receive a catalog must not emit it. The widget still loads. You never lose a page because a token row is missing.

The React renderer (@rulecms/widget-react) and the HTML custom element (which bundles that renderer) turn a non-empty catalog into one <style> scoped to that widget's published key. Nested collections inherit. Other widgets on the same page keep their own catalogs. Your html element is not restyled.

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

  • This is not a theming engine. No media queries, no [data-theme] selectors, no dark-mode switch in RuleCMS. Those belong in your stylesheet. The catalog is a flat list of names and values.
  • Class names are a different tool — unless you fill Class Definitions. A CSS class on a column still depends on a stylesheet. Variables you put in the CSS Variables catalog always preview. Class names preview in composer/Preview when they also exist in the Class Definitions catalog. See CSS Classes.
  • This catalog belongs to this widget. Copy and promote copy these columns as-is. The project catalog is only the starting snapshot.
  • Leave the catalog empty if the host owns every name. Pointers in the composer still work. Nothing extra is sent to the published page.
  • Ask whoever owns the stylesheet which names are safe. A variable defined only inside one app component will not resolve for a widget placed somewhere else — unless that name is also in thewidget catalog and this widget's inclusion is on.

Related docs