Skip to content

aa-trigger

aa-trigger controls when an animation fires. By default every animation is scroll-driven (ScrollTrigger), but for hero content above the fold — where the element is already in view at page load — you usually want it to play immediately.

ValueBehaviour
scrollDefault. Plays when the element scrolls into view (or scrubbed if aa-scrub is set).
loadPlays on every init() call, including the first. Use for SPA route entries (Next.js App Router back/forward nav). Honours aa-delay for sequencing siblings.
load-oncePlays on the first init() call in the page session — never on subsequent inits. Use for hero content under a page-transition wrapper (Barba, View Transitions). Honours aa-delay like load.
lcpLike load, but for the Largest Contentful Paint element (the hero's largest image/visual). Painted at first paint at 0.01 opacity so it counts toward LCP before the bundle loads, then fades to full. See Optimizing LCP below.
event:<name>Stays paused until an aa:trigger CustomEvent with detail.name === <name> is dispatched on it.
clickPlays on click of the element.

Inside known containers ([aa-modal-name], [aa-tabs-content], [aa-tabs-visual], [aa-slider-item]) the trigger is inferred — e.g. animations inside a slide automatically use event:slide-active. Setting aa-trigger explicitly always wins.

load and load-once entrances are paint-gated: the lib builds them paused and starts them one frame after the reveal, so the animation begins exactly at its first painted frame instead of jumping ahead while a heavy page is still laying out. The global loadDelay init option (default 0) adds an optional beat on top of any per-element aa-delay, measured from that paint — set a positive value if you want the entrance to hold briefly before playing.

If load-once fired on every init() call, every Barba (or View Transition) navigation would replay the animation while the new container is still hidden behind the transition wrapper — the user never sees it, and by the time the wrapper drops away the element is already in its final state. So load-once is intentionally one-shot: it fires on the very first init in the page lifetime (until the next hard reload), then stays out of the way.

On subsequent inits, a load-once-only element gets no animation — no scroll-trigger fallthrough, no from-state applied. The aa-ready flip at the end of init() makes it visible in its natural CSS state. This is exactly what you want for hero text under a page-transition wrapper: animate beautifully on first load, then just be present on every navigation back.

Combine with another trigger to handle subsequent navigations explicitly — see below.

Space-separated values run as a list. The load-once trigger always owns the first init cycle (and short-circuits any other triggers on that element so they can't double-fire). Non-load-once triggers wire up normally on subsequent inits. This is the canonical pattern for hero content in a page-transition setup:

<h1
aa-animate="text-slide-up"
aa-split="lines mask"
aa-trigger="load-once event:enter"
>
Hero headline
</h1>

On first page load: load-once fires immediately (or the CSS fallback plays if the bundle is slow). On every subsequent Barba navigation: the recipe's leave timeline dispatches aa:trigger { name: 'enter' } at the visual peak, and the headline animates then. No double-fire, no replay-while-hidden.

See the Webflow + Barba recipe for the full pattern.

A typical hero: headline reveals first, paragraph follows, CTA fades in last. No scrolling involved — every element fires on init() with a per-element aa-delay for the staircase.

Headline that slides up by line under a mask

Subline that follows 200ms behind the headline. Wraps to two lines so the per-line stagger reads clearly.

Call to action
<h1
aa-animate="text-slide-up"
aa-split="lines mask"
aa-trigger="load-once"
>
Headline
</h1>
<p
aa-animate="text-slide-up"
aa-split="lines mask"
aa-trigger="load-once"
aa-delay="0.2"
>
Subline 200ms behind.
</p>
<a
href="#"
aa-animate="fade"
aa-trigger="load-once"
aa-delay="0.4"
>
CTA 400ms behind.
</a>

aa-trigger="load-once" only fires once init() runs, which means once the JS bundle has downloaded, parsed, and called the lib. On a slow connection that delay is user-visible — and the lib's FOUC guard (visibility: hidden on [aa-animate]) keeps the hero invisible the whole time.

For Webflow / <script>-tag projects loading from a CDN, drop the load-fallback recipe into <head>. After a tunable timeout, hero elements fade up via CSS — preserving the same staircase you defined with aa-delay and aa-stagger. When the bundle eventually arrives the lib detects the fallback and skips its own animation, so nothing flashes or replays. (Requires defer on your CDN scripts so the body parses before the timer fires — see the recipe.)

A fade-in on your hero's largest image hurts your Largest Contentful Paint score. Chrome excludes elements that are opacity: 0 or visibility: hidden from LCP measurement — and a normal aa-trigger="load" entrance is both: the FOUC guard hides it (visibility: hidden) and the fade starts at opacity: 0. So LCP can't be recorded until the JS bundle has downloaded, parsed, run init(), and started the fade. On a slow connection that's a poor LCP, even though the image was ready much earlier.

aa-trigger="lcp" fixes this. The companion stylesheet paints the marked element at first paint at a near-invisible opacity: 0.01 — non-zero, so Chrome counts it as an LCP candidate immediately, before the bundle runs — and skips the FOUC guard for it. When the lib loads, the entrance fades it from 0.01 to full. Visually identical to a fade from 0; to Lighthouse, the LCP is recorded at first paint.

<img
aa-animate="fade"
aa-trigger="lcp"
fetchpriority="high"
src="hero.jpg"
alt=""
/>

It works with any opacity-bearing aa-animatefade, rotate-up, blur, fade-up, etc. — so the LCP element can still have a rich entrance, not just a plain fade.

fade · lcp
rotate-up · lcp

Use it correctly:

  • Put it on a single above-the-fold element — the one that is your LCP (usually the largest hero image or visual). Marking several elements defeats the purpose.
  • Keep the image in the static HTML (not injected by JS) and add fetchpriority="high" (or loading="eager") so the browser fetches it early — aa-trigger="lcp" only removes the library's own LCP regression; it can't speed up an image the browser doesn't know about yet.
  • Requires the updated inline <head> snippet. The LCP rules must apply at first paint, so they live in the inline snippet you paste, not the non-blocking external stylesheet. If you set up this site before lcp existed, re-paste the head snippet (or the load-fallback recipe).

Under the slow-network fallback the element still reveals: if init() is slow, it fades in via CSS (aa-fallback); if the bundle never arrives, it's shown at full opacity (aa-timeout).

Page transitions (Barba, View Transitions)

Section titled “Page transitions (Barba, View Transitions)”

aa-trigger="load-once" fires only on the very first init() call in the page session — on subsequent inits (Barba navigations, programmatic destroy() + init()) it's a no-op and the element renders in its natural state. That's almost always what you want for hero text under a transition wrapper: animate beautifully on first load, then just be present on every navigation back.

If you need an animation on every navigation too, combine load-once with event:enter (aa-trigger="load-once event:enter") — load-once handles first init, event:enter handles every subsequent nav. See the Webflow + Barba recipe for the full pattern.

The slow-network fallback only matters for the first page load — subsequent navigations have the bundle cached, so init runs immediately.

SPA route entry (Next.js App Router) — use load

Section titled “SPA route entry (Next.js App Router) — use load”

In a Next.js App Router setup, a single AlrdyInit client component lives in the root layout and re-calls init() on every usePathname() change. There's no transition wrapper hiding the leaving DOM — the new route is rendered cold, and the user expects the entrance animation every time they arrive on a "new" page (including via browser back/forward nav).

aa-trigger="load-once" is wrong here because it skips every init after the first; the headline lands at its final state with no animation. aa-trigger="load" is the right primitive:

<h1 aa-animate="text-tilt-up" aa-trigger="load">Welcome to /cohort</h1>
<a aa-animate="fade-up" aa-trigger="load" aa-delay="0.2">Get started</a>

The animation fires on cold load, forward nav, and back/forward — wherever init() runs.

ScenarioUse
Webflow + Barba, View Transitions, any transition wrapper covers the swapload-once (or load-once event:enter for both)
Next.js App Router, plain SPA, no transition wrapperload
Mixed: first paint and an in-page event later (modal open, etc.)combine, e.g. load event:modal-active