Contents

Widget chrome

A dark site can still show white form controls inside the embed. The published-key wrapper does not own color-scheme. Widget chrome lets RuleCMS set color-scheme, selection, caret, and scrollbars on the widget root — or leave those styles to your site.

The host owns color-scheme. The widget still looks light

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. They are separate settings.

When to use it / when not to

You wantWithout widget chromeWith this catalog
A dark widget on a light host (or the reverse) so form controls match the embedNative inputs stay the host scheme. Selection is the browser default.RuleCMS sets color-scheme on the widget root. Inputs paint dark (or light) with the widget.
A host that already sets color-scheme on a wrapper around the embedWorks today. Do not double-set it.Turn published inclusion off. Composer and Preview still use the catalog.
Color scheme is the important field. Selection, scrollbar, and caret are brand polish on the same page so you do not grow a fifth catalog later.
One chrome entry per project — not “only fields this widget uses.” Same choice as fonts and CSS variables.

The project catalog

Open an environment, then Widget chrome. The page is project-scoped: Development, Staging, and Production share it.

  • Preview dumps the catalog as emitted CSS on .rulecms-render-surface > * — not a :root listing.
  • Edit is one card: color scheme, selection colors, caret, and scrollbar fields. Pick dark or type var(--brand-accent) — not a pasted stylesheet. Invalid values are refused. Catalog Save is explicit.

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.

What the renderer does with the catalog

SurfaceWhat happens
Composer canvas and PreviewAlways injects chrome on .rulecms-render-surface > * and that subtree's ::selection. The published-inclusion flag is ignored here so authors still see native controls while they work.
Published widget (React SDK and HTML custom element)The widget fetch includes the catalog when inclusion is on. The renderer emits one <style> on [data-rulecms-published-key] > *. RuleCMS never styles :root, html, or body.

You do not republish a widget to restyle chrome. The catalog is joined when the host fetches the widget. The next uncached fetch picks up the current fields.

Published widgets

On the project's Widget chrome page, the Published widgets accordion holds two radios. Changing a radio saves immediately. Catalog Save is separate.

  • 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 the default.
  • 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.
Composer and Preview still use the catalog when inclusion is off. Only the published (and Development-draft) embed on your site stops carrying the extra rules.
If you turn inclusion off, the host must set color-scheme on a wrapper around the embed. A missing scheme on the live page leaves native inputs on the host default.

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, its collections, CSS variables, class definitions, fonts, keyframes, themes, and typed properties — not folded into itemList. The field is widgetChrome.

{
  "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.

The React renderer (@rulecms/widget-react) and the HTML custom element 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;
}

Good to know

  • One catalog per project. Not per environment, not per widget. At most one chrome entry.
  • Themes change tokens. Chrome changes native controls. A dark theme can set --background. Chrome is what makes a <select> paint dark without styling html.
  • React and the HTML embed this pass. Other framework SDKs do not emit this catalog yet.
  • Leave the catalog empty if the host already sets color-scheme around the embed. Nothing extra is sent to the published page.

Related docs