Contents

Themes

Maya already has a homepage hero. The brand colour on that hero is --brand-surface — the light value lives on CSS Variables. When her product puts dark on html, that light surface stays light. A dark: utility on the widget never sees html.dark on the host. This page is the project catalog: the template of host-condition overrides copied onto each new widget. After that the widget owns the copy. Editing here does not restyle widgets that already exist.

What you can do

You already opened Maya's homepage hero from Widgets and started composing. The brand colour on that hero is --brand-surface — the light value lives on CSS Variables. When her product puts dark on html, that light surface stays light. A dark: utility on the widget never sees html.dark on the host. This catalog is the override under that host condition.

Two everyday jobs:

  • Override --brand-surface once for the next widget. Add html.dark (or prefers-color-scheme: dark) here and the dark value for that name. A widget she creates after that starts with the wrap already filled in.
  • Leave inclusion off when the host already themes those names. Her product stylesheet can set --brand-surface under its own .dark. The hero still points at the name. RuleCMS does not ship a second wrap.

That same pattern covers prefers-color-scheme when the OS is the switch, a host that uses data-theme instead of a class, and a landing page that never loaded her dark stylesheet. Authors stay in the catalog form. Engineers keep one vocabulary.

How a theme catalog thinks

CSS Variables holds the light (or default) --name values. Those emit as one static map on the widget. That is not theming. Someone still has to override the name when the host is dark.

A class body can nest @media (prefers-color-scheme: dark), but a token cannot. And when the host puts dark on html — outside the published key — [key] .dark\:bg-black never sees it. Themes are the missing catalog: an allowlisted host condition plus the token overrides that apply under it. Light values stay on CSS Variables. This page stores overrides only.

The legal emit reads the host and applies tokens only under the published key. It never sets properties on html, :root, or body.

This catalog is the template for the next widget, not a live link to widgets that already exist. When Maya creates a widget, RuleCMS copies these overrides onto it — including the inclusion radios. After that the widget owns the copy. Composer, Preview, and the published embed read that widget, not this page. The Projects guide already draws that line. This page is the theme catalog itself.

Here is the case that surprises people. Maya adds dark:bg-black to a column on the hero and expects it to flip when the host is html.dark. It does not. The dark: utility looks for a .dark ancestor inside the published key. The host marker lives on html, outside that span. This catalog is the override that actually reads the host.

A second surprise: she edits --brand-surface under html.dark on the project and expects the Production Default Widget — the hero she already built — to pick up the dark value. It does not. Existing widgets stay on the snapshot they received at create until someone opens that widget's CSS Settings and clicks Sync from Project.

A third surprise: Preview will not flip html.dark or data-theme unless the composer chrome is already in that state. The rules are still emitted so a dark host iframe can match.

The renderer does not read this catalog. Composer, Preview, and the published widget GET read the widget's own catalog. This project catalog is copied onto a widget only when that widget is created.

A new widget receives a snapshot of this catalog, including the inclusion flags. Widgets that already exist are not updated automatically. Existing widgets can pull those updates later from the widget's own CSS Settings page. Copy, promote, and restore copy the source widget's catalog, not this one.

SurfaceWhat happens
Composer canvas and PreviewDo not read this catalog. They read the widget's draft columns.
Published widget (React SDK and HTML custom element)Reads that widget's published columns. Inclusion on that widget decides whether the embed carries them.

You need a Project CSS Catalog group at View, Manage, or Admin. View can open this page. Manage is enough to add an override and flip the inclusion radios. Those radios set the default copied onto new widgets. They do not change widgets that already exist.

The full catalog is copied onto a new widget — not “only overrides this widget uses.” Same choice as CSS variables and class definitions.

Override --brand-surface when the host is dark

You do this on environment home. The Default Widget in Production already exists — it will not pick up a new override until someone syncs. A widget she creates after this save starts with html.dark already there.

  1. Open the organization → team → project → environment you want to work in.
  2. Under CSS catalog, click Themes. You land on this project page — Development, Staging, and Production share it.
  3. Open Edit. Click Add theme. Condition: html.dark (or prefers-color-scheme: dark if the OS is the switch). Click Add variable. Name: --brand-surface. Value: the dark colour — not a pasted html.dark { … }. RuleCMS emits the wrap. Save Changes.
  4. Create a new widget from the orange Widgets card — that widget receives a snapshot of this catalog. Or open a widget that already exists, go to CSS Settings, and click Sync from Project so the hero picks up the dark override.

That is a safe stop. The next widget starts with a dark --brand-surface under the host condition instead of the light value that never flipped. Come back here when the next widget should start from a richer set of overrides, or open that widget's catalog when only the hero should change.

Condition and override rows

Edit is a condition select plus name-and-value rows — the same pair UI as CSS Variables, not a pasted CSS prelude. Reach for each field when the job matches:

FieldWhen you reach for it
ConditionOne allowlisted host marker — prefers-color-scheme: dark, prefers-color-scheme: light, html.dark, [data-theme=dark], or [data-theme=light]. Pick the one her product already uses. Do not paste a CSS prelude. RuleCMS emits the wrap.
Name and ValueOverride rows only. --brand-surface and the dark colour. The light (or default) value stays on CSS Variables. Same sanitizer as that catalog: no url(, no @, no braces, no ;.
Class-strategy hosts must put dark or data-theme on html (or another ancestor of the widget). A class inside the widget is not enough — the host marker lives outside the published key. That is why dark: utilities on the widget miss html.dark on the host.
Widget chrome is a different tool. Color-scheme and selection on the widget root tell native controls how to paint. This catalog changes token values. They do not replace each other.

The project catalog

Open an environment, then Themes. That page is the catalog for the project, not for one environment: Development, Staging, and Production share it.

To change one widget, open its CSS Settings — that guide is the override for this type.

Every new widget in the project can start from the same overrides.

  • Preview dumps the catalog as the emitted wrapped stylesheet — with .rulecms-render-surface in place of the published-key selector. That listing is for authors. It is not a :root listing, and it never paints html.
  • Edit is a condition select plus name + value rows. Add theme starts a block. Add variable starts an override row. Invalid names and values are refused per row. Save Changes stays disabled until the catalog is clean.

Each entry is one host condition and its overrides:

html.dark [data-rulecms-published-key="env---w"] > * {
  --brand-surface: #0a0a0a;
}

At most five conditions (one of each allowlisted pair). At most 200 variable pairs across all theme entries — the same cap as the token catalog. Values reuse the CSS Variables sanitizer: no url(, no @, no braces, no ;.

A project has no theme row until someone saves for the first time. Until then, a new widget starts without RuleCMS-owned overrides and relies on the light CSS Variables values — or on the host stylesheet. A dark host then leaves --brand-surface light.

Updates for existing widgets

Editing the project catalog never changes a widget by itself. A person must open the widget and click Sync from Project.

Widgets are per-environment (Development, Staging, Production), but the project catalog is shared across all three. Syncing a Production widget pulls from the same project values a Development widget would.

There is no project-side action that pushes settings to many widgets. Syncing is always started from a widget — one type from its feature page, or all nine from the hub.

Your site probably themes them too — and that is fine

The original theme model is still the right one when the host already sets the same --names under its own .dark: Maya's product stylesheet owns the wrap, and RuleCMS widgets inherit it. A Next.js host that already compiled those names is one of those cases — not a requirement, and not something you configure inside RuleCMS.

Teams that also fill the RuleCMS catalog usually keep the same --names in both places. By default RuleCMS still includes those overrides on the published widget. The rest of your page is untouched: injection reads the host and applies tokens under the published key. It never sets properties on html, :root, or body.

That default is the useful one when:

  • The widget is on a page that never themed those names (a landing page, a partner site, an email-adjacent HTML shell).
  • Editors need the published embed to match Preview even if the host forgot the dark values.
  • You are still rolling the dark wrap out on the host and the catalog is ahead of the stylesheet.

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

You do not have to choose between RuleCMS and your stylesheet. Most teams keep the same --names in both places. RuleCMS copies this catalog onto each new widget by default, including the inclusion flag, so a new embed can look correct even on a page that never themed --brand-surface. If that double definition is a problem, turn inclusion off.

Turn off inclusion on published widgets

On the project's Themes page, the Published widgets accordion holds two radios. These radios set the default copied onto new widgets. Changing them does not change widgets that already exist. Changing a radio saves immediately. Catalog Save is separate.

The first radio is selected by default:

  • Include these theme overrides when published widgets are rendered. A non-empty catalog is sent with the widget and the renderer wraps the overrides. This is how RuleCMS makes Preview and the live embed match.
  • My apps already theme these tokens. Do not include them again when the widget is being rendered. Published widgets still carry the light token names, but RuleCMS does not ship the host-condition wrap. Your host stylesheet must set the same --names under its own .dark. Use this when the app already themes --brand-surface.
Composer and Preview still use the widget catalog when inclusion is off. These radios only set the default copied onto a new widget. They do not change widgets that already exist. Empty catalog plus the setting on is the same as off: there is nothing to include.
If you turn inclusion off, the host must theme every token name the widgets use under its own dark strategy. A missing override on the live page leaves the light CSS Variables values in place. 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. When inclusion is on and the catalog is not empty, RuleCMS hangs those overrides on that same response as a sibling of the widget, its collections, CSS variables, class definitions, and fonts — not folded into itemList, not copied onto each collection. The field is themes so the renderer can emit the wrap without guessing which list is type, and without touching your html element.

{
  "success": true,
  "data": {
    "widget": { "publishedKey": "…", "itemList": "…", "…": "…" },
    "childCollections": { },
    "themes": [
      {
        "condition": { "kind": "html-class", "value": "dark" },
        "variables": { "--brand-surface": "#0a0a0a" }
      }
    ]
  }
}

themes 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 includeThemes: false so renderers that still receive a map must not emit it. Missing on the payload means include (legacy clients). The widget still loads. You never lose a page because an override 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> that reads the host and applies tokens on boxed children of the published-key span, for example:

html.dark [data-rulecms-published-key="env---w"] > * {
  --brand-surface: #0a0a0a;
}

@media (prefers-color-scheme: dark) {
  [data-rulecms-published-key="env---w"] > * {
    --brand-surface: #0a0a0a;
  }
}

Nested collections inherit. Other widgets on the same page keep their own catalogs — each wrap is scoped to that widget's published key.

The Development draft fetch (Development Integration) reads the same fields from the draft widget row, so a dev. fallback preview on your site matches composer.

After a widget is created, restyling an override means editing that widget's catalog (or the host stylesheet). Editing this page does not change widgets that already exist.

If the catalog is empty, nothing extra is emitted. Host themes and light CSS Variables values still work. Widgets that never used a RuleCMS override are unchanged.

React and the HTML embed this pass. Other framework SDKs do not emit this catalog yet.

What to read next

This page is the story and the first walkthrough. The light tokens these rows override, the chrome that paints native controls, the other catalogs, and the request your app already sends live on the pages below.

  • CSS VariablesThe light (or default) token catalog. Put --brand-surface there. This page only overrides it under a host condition.
  • Widget chromeColor-scheme and selection on the widget root so native controls match the embed. Themes change token values. Chrome does not.
  • Class DefinitionsUtility bodies; a class can nest @media, a token cannot. A dark: utility on the widget still misses html.dark on the host.
  • CSS PropertiesTyped @property so overridden tokens can interpolate. This page does not type those names.
  • FontsThe typeface catalog; another unscoped at-rule.
  • KeyframesNamed @keyframes; the same name-plus-body pattern.
  • widget ThemesThis widget's live catalog, Sync from Project, and the inclusion radios that actually ship with the embed.
  • Position try, Cascade layers, and Widget chromethe rest of the project templates on the same home. Widget copies sit under Widgets in the sidebar.
  • Generate Tailwind CSSCompile the class names this widget already uses. That flow does not replace this catalog. It is not this page.
  • Projects, Environments & TokensThe pipeline, and project CSS as the template for the next widget.
  • Development IntegrationPreview the live Development draft with a dev. token.
  • HTML Custom Element EmbedThe script tag that mounts a widget in light DOM so host themes still reach it.
  • API ReferenceThe public widget GET, including themes.
  • MDN: prefers-color-scheme

How this fits

A widget is the content. CSS Variables names --brand-surface and keeps the light value. This catalog overrides that name when the host is dark. It does not replace the widget, it does not replace Maya's stylesheet, and it never paints html. Widget chrome is color-scheme and selection on the widget root — a different job. Publishing is how a draft catalog reaches a Staging or Production token.

This catalog is the template for new widgets. It is shared across Development, Staging, and Production. The renderer reads the widget, not this page. Existing widgets are not updated automatically when you edit here; they can pull the new values from CSS Settings.

  • Light tokens stay on CSS Variables. This page is the dark (or light-media) override only. Do not put the default value here.
  • Leave the catalog empty if the host already themes the same names. Token names on elements still work. Nothing extra is sent to the published page.
  • Ask whoever owns the stylesheet which dark strategy they use. html.dark, data-theme, and prefers-color-scheme are different markers. A dark: utility on the widget misses html.dark on the host. That is the surprising case, not a slower flip.