What you can do
You already opened Maya's homepage hero from Widgets and started composing. The headline on that hero should fade in — animate-fade-in, the same utility her product stylesheet already compiles. A class that only says animation: fade-in 200ms ease is a pointer. Those frames never lived in Class Definitions.
Two everyday jobs:
- Give this hero frames so animate-fade-in runs. Add
fade-inhere and the canvas follows. Edits stay on this widget. They do not write back to the project. - Pull later project frames onto this widget. When the template moves, Sync from Project copies those animations here. Entries that exist only on this widget stay.
That same pattern covers a copied animate-enter, a bounce she already uses on sale badges, and a custom name the host never compiled. Authors stay in the catalog form. Engineers keep one vocabulary.
How a keyframes catalog thinks
A class can store animation-name: fade-in. Someone still has to define the frames. The host stylesheet can do that — Maya's product already does, in the compiled Tailwind sheet. RuleCMS can do it too, in a catalog of animation name → frame blocks.
A class body cannot hold the at-rule. Class bodies wrap as a descendant rule, so a pasted @keyframes is ignored. A token cannot hold the block either — @ and braces are rejected. That is why this catalog exists: RuleCMS emits @keyframes fade-in so Preview and the published embed match.
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 frames. Publish is how they reach the published (and dev.) embed.
Here is the case that surprises people. Maya copies animate-fade-in onto the hero and expects the headline to fade. If fade-in is missing, the class is present and nothing moves. The utility is only a pointer. This catalog is the frames.
A second surprise: she edits fade-in on the project and expects the Production Default Widget — the hero she already built — to pick up the frames. 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: @keyframes is document-global. The name is not prefixed, and the rule is not scoped to the published-key span. Two embeds that define different frames for fade-in will race — same name, last widget wins. Use the same frames, or turn inclusion off on the host that already owns the name. RuleCMS does not rename animations per published key.
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 frames or click Sync from Project. Viewing CSS settings is a separate, lower permission. A viewer sees the catalog and no edit button.
Give fade-in frames on the hero
You do this on the widget management page — the same place you open the composer. If the project already has fade-in, Sync is faster than filling the row again. If only this hero should own the frames, add them 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 Keyframes card, click Edit Keyframes (or View Keyframes 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 frames only this widget needs: open Edit, click Add keyframe, name
fade-in, type thefrom/toblocks, then Save Changes. - On Staging or Production, publish when the live embed should carry the new frames. Development streams the draft — see Development Integration.
That is a safe stop. The canvas already runs fade-in. Publish is how visitors see it. Syncing updates the draft only — the live site does not move until she publishes again.
Name and frames
Edit is a name plus a textarea for the frame blocks — not a pasted @keyframes wrapper. Reach for each field when the job matches:
| Field | When you reach for it |
|---|---|
| Name | The animation ident — fade-in, enter. Letters, numbers, -, _. No @, no dots, no spaces. The help text shows Renders as @keyframes fade-in when the name is valid. |
| Frames | The blocks only: from, to, or 0%–100%. Type from { opacity: 0 }, not @keyframes fade-in { … }. Nested at-rules and url( are refused. |
animate-fade-in in Class Definitions is only a pointer at animation-name: fade-in. This catalog is who defines fade-in.This widget's catalog
Open the widget, then CSS Settings, then Keyframes. This catalog belongs to this widget.
To change the template for the next widget you create, edit the project catalog.
Copied animate-* utilities on this widget can run the frames you define here.
- Preview dumps the catalog as emitted
@keyframesblocks — not a:rootlisting. That listing is for authors. Published widgets do not dump a global stylesheet onto your page beyond those unscoped at-rules. - Edit is a name plus a textarea for the frames. Add keyframe starts a row. Invalid names and bodies are refused per row. Save Changes stays disabled until the catalog is clean.
Each entry is one animation:
@keyframes fade-in {
from { opacity: 0 }
to { opacity: 1 }
}At most 32 animations and 4000 characters per body. Nested at-rules, url(, and a pasted @keyframes wrapper are rejected.
A new widget starts as a copy of the project keyframes catalog. Until someone saves frames here (or the project had none to copy), this widget relies on the host stylesheet — a still element if that host never registered the name.
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 Keyframes 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 defines them too — and that is fine
The original animation model is still the right one for a product that already compiles Tailwind: the host stylesheet registers @keyframes fade-in, and RuleCMS widgets inherit it. A Next.js host that already ran its Tailwind build 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 animation name in both places. By default RuleCMS still includes those frames on the published widget. The rest of your page is untouched: injection is the unscoped at-rule, not a rewritten class.
That default is the useful one when:
- The widget is on a page that never compiled those frames (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 name.
- You are still rolling the animation 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, or if two embeds would race on the same name.
fade-in. If that double definition is a problem, turn inclusion off.Turn off inclusion on published widgets
On this widget's Keyframes 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 keyframes when published widgets are rendered. A non-empty catalog is sent with the widget and the renderer defines the frames. This is how RuleCMS makes Preview and the live embed match.
- 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
@keyframesblocks. Your host stylesheet must define them. Use this when the app already compilesfade-in.
@keyframes on the live page paints as a still element. 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 frames 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 keyframes so the renderer can emit the at-rule without guessing which list is type.
{
"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. You never lose a page because a frame 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> of unscoped @keyframes rules, for example:
@keyframes fade-in {
from { opacity: 0 }
to { opacity: 1 }
}Nested collections inherit. Other widgets on the same page keep their own catalogs — until two of them register the same name. Same name, last widget wins.
The Development draft fetch (Development Integration) reads the same fields from the draft widget row, so a dev. animation preview on your site matches composer.
The canvas already runs fade-in when the row is saved. Publish is how those draft frames 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 frames still work. Widgets that never used a RuleCMS animation 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 class that points at a name, the other catalogs, and the request your app already sends live on the pages below.
- Class Definitions — A class named
animate-fade-inis only a pointer. The body still needs frames this catalog defined. - Fonts — The typeface catalog; another unscoped at-rule.
- CSS Properties — Typed
@propertyso token values in these frames (and in transitions) can interpolate. This page does not type those names. - CSS Variables — The token catalog those interpolable names live in.
- Keyframes — The project template for the next widget you create.
- Position try, Cascade layers, Themes, 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 can add named frames here; it 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 animations still reach it.
- API Reference — The public widget GET, including
keyframes. - MDN: @keyframes
How this fits
A widget is the content. This catalog gives copied animate-* utilities frames to run. It does not replace the widget, and it does not replace Maya's stylesheet. Publishing is how a draft catalog reaches a Staging or Production token. The Composer is where a class name stores the pointer at fade-in.
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.
- Same name, last widget wins.
@keyframesis document-global. Two embeds that define different frames forfade-inwill 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. - Leave the catalog empty if the host owns every animation. Class names on elements still work. Nothing extra is sent to the published page.
- Ask whoever owns the stylesheet which animation names are already compiled. A missing
@keyframes fade-inon the live page paints as a still element. That is the surprising case, not a slower fade.