What you can do
You already opened Maya's homepage hero from Widgets. Themes can darken --brand-surface when her product puts dark on html. The email field and the highlighted caption on that hero still look light. Host color-scheme on body does not paint native inputs inside the widget. This catalog is color-scheme and selection on the widget root.
Two everyday jobs:
- Set color-scheme once for the next widget. Pick
darkhere. A widget she creates after that starts with native inputs that paint with the hero, not with the host page. - Leave inclusion off when the host already wraps the embed. Her product stylesheet can set
color-schemeon a wrapper around the widget. RuleCMS does not ship a second chrome rule.
That same card covers selection colors, caret, and scrollbars when the brand needs polish on the same root. Color scheme is the field that changes how a <select> and an email input paint. Themes change token values. Chrome tells the browser how native controls look. They do not replace each other.
How widget chrome thinks
The published-key wrapper is display: contents. It does not generate a box. Host body or :root can set color-scheme: dark, a custom selection color, and scrollbar styles — and native inputs inside the widget still paint light. A utility class on one inner node does not cover the whole embed. Selection stays the browser default.
A class definition can nest &::selection if someone puts that class on every root box. Authors will not. Fonts already solved “apply to the widget” with one flag. Widget chrome is that same slot for color-scheme, selection, caret, and scrollbars.
Themes change token values under a dark host. Chrome tells the browser how native controls should look. A dark --brand-surface does not make a <select> paint dark. You still need color-scheme on the widget root.
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 fields 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 chrome catalog itself.
Here is the case that surprises people. Maya puts color-scheme: dark on body and expects the email field on the hero to paint dark. It does not. The published-key span is display: contents. Host color-scheme never reaches the inputs inside. This catalog sets color-scheme on the widget root boxes — > * — so those controls follow the widget.
A second surprise: she sets dark on the project and expects the Production Default Widget — the hero she already built — to pick up dark inputs. 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: she overrides --brand-surface under html.dark on Themes and expects the email field to flip with the surface. Themes change tokens. Chrome is what makes a native control paint dark without styling html.
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.
| Surface | What happens |
|---|---|
| Composer canvas and Preview | Do 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 set color-scheme and flip the inclusion radios. Those radios set the default copied onto new widgets. They do not change widgets that already exist.
Set color-scheme so native controls match the hero
You do this on environment home. The Default Widget in Production already exists — it will not pick up a new color-scheme until someone syncs. A widget she creates after this save starts with dark already there.
- Open the organization → team → project → environment you want to work in.
- Under CSS catalog, click Widget chrome. You land on this project page — Development, Staging, and Production share it.
- Open Edit. Color scheme:
dark(orlight darkif the widget should follow the OS). Selection, caret, and scrollbar fields are optional — typevar(--brand-accent), not a pasted stylesheet. Save Changes. - 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 dark inputs.
That is a safe stop. The next widget starts with native controls that match the hero instead of the host default. Come back here when the next widget should start from richer selection or scrollbar polish, or open that widget's catalog when only the hero should change.
Color-scheme, selection, caret, and scrollbar fields
Edit is one card — not a pasted stylesheet. Reach for each field when the job matches:
| Field | When you reach for it |
|---|---|
| Color scheme | The important field. Native inputs follow this. light, dark, or light dark. Pick dark when the hero is dark and the host is not — or the reverse. |
| Selection background and Selection color | Brand polish for highlighted text. A token such as var(--brand-accent) and #fff. Same sanitizer as CSS Variables: no url(, no @, no braces, no ;. |
| Caret color | The text-insertion caret on the email field. Another color or token — the same sanitizer. |
| Scrollbar width and Scrollbar color | Width is auto, thin, or none. Color is auto, or two colors such as var(--brand-accent) transparent. |
The project catalog
Open an environment, then Widget chrome. 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 chrome.
- Preview dumps the catalog as emitted CSS on
.rulecms-render-surface > *— with that class in place of the published-key selector. That listing is for authors. It is not a:rootlisting, and it never paintshtml. - Edit is one card: color scheme, selection colors, caret, and scrollbar fields. Pick
darkor typevar(--brand-accent)— not a pasted stylesheet. Invalid values are refused. Save Changes stays disabled until the catalog is clean.
A typical emit looks like this:
[data-rulecms-published-key="env---w"] > * {
color-scheme: dark;
caret-color: var(--brand-accent);
scrollbar-width: thin;
scrollbar-color: var(--brand-accent) transparent;
}
[data-rulecms-published-key="env---w"] > * ::selection {
background: var(--brand-accent);
color: #fff;
}Color values reuse the CSS variable sanitizer: no url(, @, braces, or ;. Color scheme is light, dark, or light dark. Scrollbar width is auto, thin, or none. An empty card is no entry. At most one chrome entry — this catalog is a single card, not a list.
A project has no chrome row until someone saves for the first time. Until then, a new widget starts without RuleCMS-owned color-scheme and relies on the host. A dark host then still leaves native inputs light inside the embed.
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 sets color-scheme too — and that is fine
The original model is still the right one when the host already sets color-scheme on a wrapper around the embed: Maya's product stylesheet owns the scheme, and RuleCMS widgets inherit it. A Next.js host that already painted those controls 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 scheme in both places. By default RuleCMS still includes those fields on the published widget. The rest of your page is untouched: injection applies chrome 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 set
color-schemearound the embed (a landing page, a partner site, an email-adjacent HTML shell). - Editors need the published embed to match Preview even if the host forgot native-control styles.
- You are still rolling dark chrome out on the host and the catalog is ahead of the stylesheet.
It is the wrong default when the host wrapper is the only definition you want at runtime — especially if RuleCMS and the host might disagree for a while.
color-scheme around the widget. If that double definition is a problem, turn inclusion off.Turn off inclusion on published widgets
On the project's Widget chrome 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 this widget chrome when published widgets are rendered. A non-empty catalog is sent with the widget and the renderer applies it. This is how RuleCMS makes Preview and the live embed match.
- My apps already set color-scheme and selection styles. Do not include them again when the widget is being rendered. Published widgets still carry token names that need those colors, but RuleCMS does not ship the chrome rules. Your host stylesheet must set them. Use this when the app already wraps the embed with
color-scheme.
color-scheme on a wrapper around the embed. A missing scheme on the live page leaves native inputs on the host default. 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 fields on that same response as a sibling of the widget, its collections, CSS variables, class definitions, fonts, keyframes, themes, and typed properties — not folded into itemList, not copied onto each collection. The field is widgetChrome so the renderer can emit color-scheme and selection on the widget root without guessing which list is type, and without touching your html element.
{
"success": true,
"data": {
"widget": { "publishedKey": "…", "itemList": "…", "…": "…" },
"childCollections": { },
"widgetChrome": {
"colorScheme": "dark",
"selectionBackground": "var(--brand-accent)",
"selectionColor": "#fff",
"caretColor": "var(--brand-accent)",
"scrollbarColor": "var(--brand-accent) transparent",
"scrollbarWidth": "thin"
}
}
}widgetChrome 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 includeWidgetChrome: false so renderers that still receive an object must not emit it. Missing on the payload means include (legacy clients). The widget still loads. You never lose a page because a chrome field 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> on the widget root, for example:
[data-rulecms-published-key="env---w"] > * {
color-scheme: dark;
}
[data-rulecms-published-key="env---w"] > * ::selection {
background: var(--brand-accent);
color: #fff;
}Nested collections inherit. Other widgets on the same page keep their own catalogs — each rule 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 chrome 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 color-scheme still works. Widgets that never used RuleCMS chrome 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 tokens chrome can point at, the theme overrides that change those tokens, the other catalogs, and the request your app already sends live on the pages below.
- Themes — Token overrides under a dark host. Themes change tokens. Chrome tells native controls how to look.
- CSS Variables — Tokens chrome can point at, such as
var(--brand-accent). The light (or default) value lives there. - Fonts — The same apply-to-widget slot; another catalog that lands on the widget root instead of
:root. - Class Definitions — Utility bodies; a class can nest
::selectionif you put it on every root box. Authors will not. - CSS Properties — Typed
@propertyso tokens can interpolate. This page does not type those names. - Keyframes — Named
@keyframes; the same name-plus-body pattern. - widget Widget chrome — This widget's live catalog, Sync from Project, and the inclusion radios that actually ship with the embed.
- Position try, Cascade layers, and Themes — 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 color-scheme still reaches it — except native inputs inside the embed, which still need this catalog.
- API Reference — The public widget GET, including
widgetChrome. - MDN: color-scheme
How this fits
A widget is the content. Themes override --brand-surface when the host is dark. This catalog sets color-scheme and selection on the widget root so native controls match the embed. It does not replace the widget, it does not replace Maya's stylesheet, and it never paints html. Themes change tokens. Chrome does not. 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.
- Themes change tokens. Chrome changes native controls. A dark theme can set
--background. Chrome is what makes a<select>paint dark without stylinghtml. - Leave the catalog empty if the host already sets color-scheme around the embed. Nothing extra is sent to the published page.
- Host
color-schemeonbodydoes not reach inputs inside the widget. The published-key wrapper isdisplay: contents. That is the surprising case, not a slower flip.