Animations

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.

Key files

Preset shape

interface 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
}

Available presets (live source: 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): cssChainSpotlight13, cssChainLabel12, cssGridSpotlight14, … (see source for full list).

Usage

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.

Rules (canonical, do not violate)

From .claude/rules/templates.md — Animations preset system:

  • NEVER add @keyframes or --animate-* CSS vars to globals.css, styles.css, theme.css, or any new CSS file.
  • NEVER put animate-* classes in template/block className — use root.animation so it's user-adjustable.
  • NEVER create template-specific or one-off animation CSS files.
  • New animation patterns must be added as a preset in animations.ts + matching @keyframes in packages/sdk/src/css/styles.css.
  • See ANIMATION_RULES in utils/ai/prompt-rules.ts for the canonical list shipped to the AI.

Editor preview tile

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

Marquee gotcha

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).

Related