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:
- Darken this hero when the host is dark. Add
html.darkhere and the dark value for--brand-surface. The canvas follows. Edits stay on this widget. They do not write back to the project. - Pull later project overrides onto this widget. When the template moves, Sync from Project copies those conditions here. Entries that exist only on this widget stay.
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 belongs to this widget. A new widget starts as a snapshot of the project template. After that, edits here stay here. Composer and Preview emit these draft wraps. Publish is how they reach the published (and dev.) embed.
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.
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. Later project edits do not come back here unless you click Sync from Project.
| 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. |
You need widget edit for this environment — the same permission as the composer — to change overrides or click Sync from Project. Viewing CSS settings is a separate, lower permission. A viewer sees the catalog and no edit button.
Override --brand-surface when the host is dark
You do this on the widget management page — the same place you open the composer. If the project already has the html.dark wrap, Sync is faster than filling the row again. If only this hero should own the override, add it here.
- Open the organization → team → project → environment → Maya's homepage hero (or the Default Widget).
- Click CSS Settings. You land on Widget CSS Settings.
- On the Themes card, click Edit Themes (or View Themes to scan first). The Sync from Project button also lives on this catalog page and on the hub.
- To pull the project template: click Sync from Project. To add an override only this widget needs: open Edit, click Add theme, pick
html.dark, add--brand-surfaceand its dark value, then Save Changes. - On Staging or Production, publish when the live embed should carry the new wraps. Development streams the draft — see Development Integration.
That is a safe stop. The canvas already emits the wrap. Publish is how visitors see it. Syncing updates the draft only — the live site does not move until she publishes again.
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:
| Field | When you reach for it |
|---|---|
| Condition | One 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 Value | Override 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 ;. |
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.This widget's catalog
Open the widget, then CSS Settings, then Themes. This catalog belongs to this widget.
To change the template for the next widget you create, edit the project catalog.
Tokens on this widget can flip under the host conditions you define here.
- Preview dumps the catalog as the emitted wrapped stylesheet — with
.rulecms-render-surfacein place of the published-key selector. That listing is for authors. It is not a:rootlisting, and it never paintshtml. - 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 new widget starts as a copy of the project theme catalog. Until someone saves overrides here (or the project had none to copy), this widget relies on the light CSS Variables values — or on the host stylesheet. A dark host then leaves --brand-surface light.
Sync from the project
Once this widget exists, its CSS settings are its own. The project catalog does not update them automatically. A Sync from Project button lives on this Themes page — preview and edit — and on the widget's CSS Settings hub.
Syncing copies the project's version of an entry onto this widget. When both sides share an id, the project's value replaces the widget's. Entries that exist only on this widget are kept.
The button reads Synced with Project and is disabled when there is nothing to pull. Tiles on the CSS Settings hub show Project updates available when the project has updates for that type.
Syncing updates the draft. Publish the widget for live embeds to change. You can sync one type from this page or all nine from the hub.
Editing the project catalog never changes a widget by itself. A person must 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.
When the button is disabled
- Unsaved edits — the helper reads Save or discard your changes before syncing. That stop exists so a later Save cannot overwrite what was just synced.
- No project catalog — the helper reads This project has no CSS settings to sync.
- Already in sync — the button itself reads Synced with Project and stays disabled.
Who can sync
Sync is governed by the widget edit permission for that environment — the same permission needed to change the widget in the composer. Viewing CSS settings is a separate, lower permission, and a viewer sees no button at all. Project CSS permission is not required: anyone who can edit the widget can pull project values onto it.
- Missing permission — users who can view CSS settings but cannot edit the widget in that environment do not see the button.
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.
--names in both places. RuleCMS includes this widget's theme catalog on the published embed by default so the embed looks 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 this widget's Themes 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 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
--namesunder its own.dark. Use this when the app already themes--brand-surface.
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.
The canvas already emits the wrap when the row is saved. Publish is how those draft overrides reach the live embed. (Published traffic may sit in widget-cache for a short TTL — the same delay itemList already has.)
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 Variables — The light (or default) token catalog. Put
--brand-surfacethere. This page only overrides it under a host condition. - Widget chrome — Color-scheme and selection on the widget root so native controls match the embed. Themes change token values. Chrome does not.
- Class Definitions — Utility bodies; a class can nest
@media, a token cannot. Adark:utility on the widget still misseshtml.darkon the host. - CSS Properties — Typed
@propertyso overridden tokens can interpolate. This page does not type those names. - Fonts — The typeface catalog; another unscoped at-rule.
- Keyframes — Named
@keyframes; the same name-plus-body pattern. - Themes — The project template for the next widget you create.
- Position try, Cascade layers, and Widget chrome — the rest of the project templates on the same home. Widget copies sit under Widgets in the sidebar.
- Generate Tailwind CSS — Compile the class names this widget already uses. That flow does not replace this catalog. It is not this page.
- Projects, Environments & Tokens — The pipeline, and project CSS as the template for the next widget.
- Development Integration — Preview the live Development draft with a
dev.token. - HTML Custom Element Embed — The script tag that mounts a widget in light DOM so host themes still reach it.
- API Reference — The 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 belongs to this widget. Copy and promote copy these columns as-is. The project catalog is only the starting snapshot. Use Sync from Project on this page or the CSS Settings hub to pull later project updates.
- 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, andprefers-color-schemeare different markers. Adark:utility on the widget misseshtml.darkon the host. That is the surprising case, not a slower flip.