Contents

Keyframes

A class that only says animation: fade-in 200ms is a pointer. Until the project catalog defines those frames, nothing moves. Project keyframes let RuleCMS emit @keyframes so composer, Preview, and published widgets can run the animation — or leave the names to your site.

animate-* is a pointer. This catalog is the frames

Host Tailwind (or any design-system sheet) registers @keyframes enter. Utilities only say animation-name: enter. Copy the class body into RuleCMS and nothing moves: the name is undefined.

A class cannot hold the at-rule. Class bodies wrap as [published-key] .name { … }. @keyframes inside that rule is ignored. A token cannot hold the block either — @ and braces are rejected. Project keyframes are the missing catalog: a project-wide map of animation name → frame blocks. Fill it once and composer, Preview, and — by default — published widgets emit the at-rule.

This is the sibling of Class Definitions and Fonts. Classes keep saying animation: fade-in 200ms ease. This catalog is who defines fade-in.

When to use it / when not to

You wantWithout project keyframesWith this catalog
A copied animate-* utility that actually runs on a host that never compiled those framesThe class is present. Nothing moves unless the host CSS already defines the name.RuleCMS ships @keyframes fade-in with the widget. The embed matches Preview.
A host that already ships the same animationsWorks today. Do not double-define.Turn published inclusion off. Composer and Preview still use the catalog.
The full project catalog is included — not “only keyframes this widget uses.” Same choice as CSS variables and class definitions.

The project catalog

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

  • Preview dumps the catalog as emitted @keyframes blocks — not a :root listing.
  • Edit is a name plus a textarea for the frames. Type from { opacity: 0 } and to { opacity: 1 }, not @keyframes fade { … }. Invalid names and bodies are refused per row. Catalog Save is explicit.

Each entry is one animation:

@keyframes fade-in {
  from { opacity: 0 }
  to { opacity: 1 }
}

Names are CSS custom-idents: letters, numbers, -, _. No @, no dots, no spaces. At most 32 animations; 4000 characters per body. Nested at-rules, url(, and a pasted @keyframes wrapper are rejected.

What the renderer does with the catalog

SurfaceWhat happens
Composer canvas and PreviewAlways injects an unscoped @keyframes stylesheet. The published-inclusion flag is ignored here so authors still see the animation 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> of @keyframes rules. The at-rule is document-global — it is not scoped to the published-key span, and the name is not prefixed.

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

Published widgets

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

  • Include these keyframes when published widgets are rendered. A non-empty catalog is sent with the widget and the renderer defines the frames. This is the default.
  • My apps already define these animations. Do not include them again when the widget is being rendered. Published widgets still carry class names that point at those animations, but RuleCMS does not ship the @keyframes blocks. Your host stylesheet must define 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 define every animation name the widgets use. A missing @keyframes on the live page paints as a still element.

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, and fonts — not folded into itemList.

{
  "success": true,
  "data": {
    "widget": { "publishedKey": "…", "itemList": "…", "…": "…" },
    "childCollections": { },
    "keyframes": {
      "fade-in": "from { opacity: 0 }\nto { opacity: 1 }"
    }
  }
}

keyframes 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 includeKeyframes: false so renderers that still receive a map 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>, for example:

@keyframes fade-in {
  from { opacity: 0 }
  to { opacity: 1 }
}

Good to know

  • One catalog per project. Not per environment, not per widget.
  • Same name, last widget wins. @keyframes is document-global. Two embeds that define different frames for fade-in will race. Use the same frames, or turn inclusion off on the host that already owns the name. RuleCMS does not rename animations per published key.
  • React and the HTML embed this pass. Other framework SDKs do not emit this catalog yet.
  • Leave the catalog empty if the host owns every animation. Class names on elements still work. Nothing extra is sent to the published page.

Related docs