Animation
Animate your page on load, and your content as it scrolls into the frame.
Geppetto has a one-shot entrances animation helper: a headline fades up, a divider draws across, a row of cards staggers in. You just say which elements animate and how they move.
The Animation Partial
Wrap the content you want to animate in {{#> Animation}} … {{/Animation}}
{{#> Animation}}
<h1
x-show="show"
x-transition:enter="transition ease-out duration-1000"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
>
{{headline}}
</h1>
{{/Animation}}
The three transition attributes
The partial gives you a reactive show value. Every element that should animate opts in with two things:
x-show="show": ties the element's visibility to the animation.x-transition:enter*classes: describe the motion (the "before" and "after" states).
Elements without x-show="show" render normally, with no animation. So you can animate just the parts you want and leave the rest static.
The three transition attributes:
x-transition:entersets the timing: easing and duration (e.g.transition ease-out duration-1000).x-transition:enter-startis the before state: how the element looks at the start (e.g.opacity-0 translate-y-4).x-transition:enter-endis the after state: where it lands (e.g.opacity-100 translate-y-0).
Staggered Reveals
To reveal a sequence (headline, then subhead, then button) give everything the sameshowstate but add a Tailwind delay-* class to each element'sentertiming:
{{#> Animation}}
<h1
x-show="show"
x-transition:enter="transition ease-out duration-1000"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
>
{{headline}}
</h1>
<p
x-show="show"
x-transition:enter="transition ease-out duration-1000 delay-200"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
>
{{subhead}}
</p>
<div
x-show="show"
x-transition:enter="transition ease-out duration-1000 delay-400"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
>
{{> Button cta}}
</div>
{{/Animation}}
Stagger steps
Each staggered element starts later than the one before. Good stagger steps: delay-100, delay-200, delay-300, delay-500, delay-700. For a 1000ms entrance, ~200ms steps feel snappy without overlapping awkwardly.
Motion Cookbook
Mix and match these enter-start → enter-end pairs to build any common entrance. Pair them with a timing class like x-transition:enter="transition ease-out duration-1000".
<!-- Fade -->
enter-start="opacity-0"
enter-end="opacity-100"
<!-- Fade + slide up -->
enter-start="opacity-0 translate-y-4"
enter-end="opacity-100 translate-y-0"
<!-- Fade + slide from the left -->
enter-start="opacity-0 -translate-x-8"
enter-end="opacity-100 translate-x-0"
<!-- Scale in -->
enter-start="opacity-0 scale-95"
enter-end="opacity-100 scale-100"
<!-- Blur reveal (focuses in) -->
enter-start="opacity-0 blur-md"
enter-end="opacity-100 blur-none"
<!-- Line draw (for a horizontal divider) -->
enter-start="opacity-0 scale-x-0"
enter-end="opacity-100 scale-x-100"
<!-- Rotate in (subtle) -->
enter-start="opacity-0 -rotate-3"
enter-end="opacity-100 rotate-0"
Composing With Section
The Animation wrapper is invisible to layout, so you can wrap any subset of a block's content. A common shape is a Section (your block's outer wrapper) containing one or more Animation regions:
{{#> Section class="h-160 !py-10"}}
{{#> Animation}}
{{#if video-background}}
<video
src="{{video-background}}"
autoplay muted loop playsinline
class="absolute inset-0 w-full h-full object-cover"
x-show="show"
x-transition:enter="transition ease-out duration-1000"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
></video>
<div class="absolute inset-0 bg-linear-to-t from-base via-base/30 to-transparent"></div>
{{/if}}
<div class="relative z-1 mt-36">
{{#if headline}}
<div class="flex flex-col gap-8">
<h1
class="heading-xl text-fg"
x-show="show"
x-transition:enter="transition ease-out duration-1000"
x-transition:enter-start="opacity-0 blur-md translate-y-4"
x-transition:enter-end="opacity-100 blur-none translate-y-0"
>
{{headline}}
</h1>
<div
class="h-px bg-fg/30 origin-left"
x-show="show"
x-transition:enter="transition ease-out duration-1000"
x-transition:enter-start="opacity-0 scale-x-0"
x-transition:enter-end="opacity-100 scale-x-100"
></div>
</div>
{{/if}}
{{#if subhead}}
<p
class="heading-sm text-accent-2 md:pt-8"
x-show="show"
x-transition:enter="transition ease-out duration-1000 delay-200"
x-transition:enter-start="opacity-0 blur-md translate-y-4"
x-transition:enter-end="opacity-100 blur-none translate-y-0"
>
{{subhead}}
</p>
{{/if}}
</div>
{{/Animation}}
{{/Section}}
In the Editor
Animations are designed to stay out of your way while you work. In the page editor and block chat, content shows in its final, animated-in state immediately, no replaying every time you type. Use the play button in the lower right corner of the preview to play animations as you create them. On deployed sites animations will play on load and scroll enter.
Gotchas
Add x-show="show" to every element you want to animate. The partial provides the state, but it doesn't bind your elements for you. An element without x-show just renders normally.
Don't nest Animation partials. Two nested wrappers fight over the same show state and the inner one usually won't play. Use sibling Animation blocks side by side instead.
Use standard Tailwind classes, not arbitrary values. opacity-0, translate-y-4, scale-95, blur-md, delay-200, yes. translate-y-[37px], avoid.
Animation is one-shot. It plays the entrance once. For motion that tracks scrolling continuously, or for click-to-open accordions, this partial isn't the tool.
Scroll fires at 15% visibility. With trigger="scroll", the animation plays as soon as ~15% of the wrapper has entered the viewport, not when it's fully on screen. That's usually what you want (the motion is already underway as the section arrives), but it's the answer to "why did it fire so early?"
The wrapper doesn't affect layout. The partial renders as a display: contents element, so it generates no box of its own. Its children behave as if they're direct children of whatever surrounds the partial. Inside a flex or grid Section, the animated elements become flex/grid items directly. This is why a positioning context (relative) belongs on an inner element, not on the Animation wrapper, see the video-background example above, where relative z-1 sits on the inner div.
When Not to Use Animation
Hover effects (a button glow, an image zoom on hover) → use CSS
transition-*classes with Tailwind'shover:variants. No Animation partial needed.Continuous loops (a pulsing dot, an infinite marquee) → use Tailwind's
animate-*utilities likeanimate-pulseandanimate-spin, or custom CSS@keyframes.Click-triggered reveals (accordions, expanding cards) → write a small Alpine toggle inline (
x-data="{ open: false }"). See Template Interactivity.