All component animations go through the preset system in packages/sdk/src/utils/animations/animations.ts. Authors set root.animation to a preset key; users tune duration / delay / easing in the toolbar.
All component animations go through the preset system in packages/sdk/src/utils/animations/animations.ts. Authors set root.animation to a preset key; users tune duration / delay / easing in the toolbar.
cssAnimationPresets, getCSSAnimationProps, scrollAnimRef, isCSSAnimation@keyframes definitions matching each presetinterface CSSAnimationPreset {
animateClass: string; // Tailwind animate-* class
trigger: "scroll" | "load" | "hover" | "continuous";
label: string;
group: "Entrance" | "Hover" | "Continuous";
hoverClass?: string; // hover presets use a class instead of @keyframes
}
cssAnimationPresets)Entrance (scroll-triggered):
cssFadeIn, cssFadeUp, cssFadeDown, cssFadeLeft, cssFadeRight, cssScaleUp, cssBlurIn, cssSlideUp, cssFlipIn, cssSpring, cssBounceIn, cssThudIn, cssTileFlip.
Hover:
cssHoverGrow, cssHoverLift, cssHoverGlow, cssHoverPress.
Continuous:
cssSpin, cssPulse, cssWiggle, cssMarquee, cssMarqueeSlow.
Chains (multi-step coordinated entries):
cssChainSpotlight1–3, cssChainLabel1–2, cssGridSpotlight1–4, … (see source for full list).
In CraftJS JSON:
{
"type": "Container",
"props": {
"className": "py-space-lg",
"root": { "animation": "cssFadeUp" }
}
}
Duration / delay / easing are inline CSS properties (animation-duration, animation-delay, animation-timing-function) so user slider values work without generating extra classes.
From .claude/rules/templates.md — Animations preset system:
@keyframes or --animate-* CSS vars to globals.css, styles.css, theme.css, or any new CSS file.animate-* classes in template/block className — use root.animation so it's user-adjustable.animations.ts + matching @keyframes in packages/sdk/src/css/styles.css.ANIMATION_RULES in utils/ai/prompt-rules.ts for the canonical list shipped to the AI.The Animations popover (AnimationsInput.tsx) renders a preview stage above the engine tabs. The tile is a click-to-replay button — clicking it bumps a counter that's part of the React key, forcing a remount so entrance presets play again. Without this, animations with fill-mode: both (Blur In, Fade Up, …) end at their final state on first play and look static when you tweak duration / delay / easing. Type changes already retrigger via the type itself being in the key.
Scroll-triggered presets (most entrance ones) are force-played in the preview by stripping ph-anim-scroll and adding ph-in-view to the tile's className — otherwise the IntersectionObserver would gate the play behind viewport visibility.
When no preset is selected, the stage shows a dashed placeholder with "Pick an animation to preview".
cssMarquee animates 0 → -50%, so duplicated children with gap-X land half a gap off → visible jump. Use [&>*]:mr-X instead. For image marquees, drop the Container "Image Marquee" preset (sets cssMarquee animation + [&>*]:mr-X automatically).