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 want | Without widget chrome | With this catalog |
|---|---|---|
| A dark widget on a light host (or the reverse) so form controls match the embed | Native 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 embed | Works today. Do not double-set it. | Turn published inclusion off. Composer and Preview still use the catalog. |
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:rootlisting. - 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. 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
| Surface | What happens |
|---|---|
| Composer canvas and Preview | Always 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.
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 stylinghtml. - 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
- Themes — token overrides under a dark host; chrome tells native controls how to look
- CSS Variables — tokens chrome can point at, such as
var(--brand-accent) - Fonts — the same apply-to-widget slot
- Class Definitions — utility bodies; a class can nest
::selectionif you put it on every root box - Cascade layers — wrap those class rules in
@layer utilitiesor@layer components - CSS Properties — typed
@property - Keyframes — named
@keyframes - Position try — named
@position-tryfallbacks - HTML Custom Element Embed
- Development Integration
- API Reference — the public widget GET, including
widgetChrome - Projects, Environments & Tokens
- The Composer