Contents

CSS Variables

Name a colour, a spacing step, or a shadow once — then point every widget at that name. RuleCMS stores the pointer on the setting, keeps the values on the project, and the renderer shows them in the composer, in Preview, and on the published page.

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 the project. Add the catalog once. The composer, Preview, and the published widget then show those values — not just store the pointers. 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 in the project catalog and every widget that uses it follows, without opening each one, without republishing for the token itself, and without teaching marketers a hex code they will mistype.

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 the project 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 the project catalog on published widgets 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.
The project catalogEnvironment home → CSS VariablesComposer, Preview, and — by default — every published widget in that project.

The widget JSON never stores the hex. It stores the name. Definitions live on the project, or on the host, or both. That is why changing a token does not require opening every widget: nothing on the widget document copied the value.

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 the project 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.

The project catalog

Open an environment, then CSS Variables. That page is the catalog for the project, not for one environment: Development, Staging, and Production share it. Every widget in the project can point at the same names.

  • 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 project CSS variables; 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

Once names exist on the project, RuleCMS does not leave them as a document only editors can read. The renderer applies them wherever a widget from that project is shown inside RuleCMS, and — by default — on your site too.

SurfaceWhat happens
Composer canvasThe catalog is loaded once for the project and applied to the render surface. A column that points at --brand-surface paints that colour while you edit.
PreviewSame catalog, same idea: Preview is an honest picture of the published widget, including tokens you defined in RuleCMS.
Published widget (React SDK and HTML custom element)The widget fetch includes the catalog. The renderer emits one scoped <style> so those names exist inside that widget. 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.

You do not republish a widget to restyle a token. The catalog is joined when the host fetches the widget. The next uncached fetch picks up the current names. (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 the project's CSS Variables page, the Published widgets accordion at the top holds two radios. The first 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 the 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) does the same join, 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.
  • One catalog per project. There is not a separate list per environment. Promote and publish the widgets; the tokens stay project-wide.
  • 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 the project catalog and inclusion is on.

Related docs