Skip to content

Webflow

This guide is for Webflow sites with no page-transition library. If you use Barba, follow the Webflow + Barba guide instead — the script setup is the same, but the init wiring is different.

There are three pieces, mapped to three places in Webflow:

Webflow locationWhat goes here
Project Settings → Custom Code → Head CodeSlow-network fallback, lib CSS, GSAP/Lenis/alrdy-animate scripts.
Project Settings → Custom Code → Footer CodeThe init() call. Optional: global GSAP timelines that should run on every page.
Page Settings → Custom Code → Before </body> tagPage-specific custom GSAP timelines.

Paste this into Project Settings → Custom Code → Head Code. It loads everything in parallel via defer, with an inline FOUC + slow-network fallback that fires before any external script lands.

<!-- Slow-network + init-timeout safety (optional but recommended).
Two layers: aa-fallback fades aa-trigger="load" elements at FALLBACK_DELAY
(graceful hero staircase). aa-timeout reveals every animated element at
TIMEOUT_DELAY (universal safety if init never arrives). See
/recipes/load-fallback/ for the full explanation. -->
<script>
// Tune these two numbers without reading the rest of the snippet.
const FALLBACK_DELAY = 1000; // ms — hero copy fades in via CSS keyframe
const TIMEOUT_DELAY = 4000; // ms — universal reveal if init never runs
const applyFallback = () => {
if (document.documentElement.hasAttribute('aa-loaded')) return;
document.documentElement.setAttribute('aa-fallback', '');
document.querySelectorAll('[aa-trigger~="load"][aa-animate]').forEach((el) => {
const stagger = parseFloat(el.getAttribute('aa-stagger'));
const baseDelay = parseFloat(el.getAttribute('aa-delay')) || 0;
if (Number.isFinite(stagger) && el.children.length > 0) {
el.style.animation = 'none';
el.style.visibility = 'visible';
Array.from(el.children).forEach((child, i) => {
child.style.animation = 'aa-fallback-appear 0.5s ease both';
child.style.animationDelay = (baseDelay + i * stagger) + 's';
});
} else if (baseDelay > 0) {
el.style.animationDelay = baseDelay + 's';
}
});
};
setTimeout(() => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', applyFallback, { once: true });
} else {
applyFallback();
}
}, FALLBACK_DELAY);
setTimeout(() => {
if (document.documentElement.hasAttribute('aa-loaded')) return;
document.documentElement.setAttribute('aa-timeout', '');
}, TIMEOUT_DELAY);
</script>
<style>
[aa-animate]:not([aa-ready]) { visibility: hidden; }
@keyframes aa-fallback-appear {
from { opacity: 0; transform: translateY(1.5rem); }
to { opacity: 1; transform: translateY(0); }
}
html[aa-fallback] [aa-trigger~="load"][aa-animate]:not([aa-ready]) {
visibility: visible;
animation: aa-fallback-appear 0.5s ease both;
}
html[aa-timeout] [aa-animate]:not([aa-ready]) { visibility: visible; }
</style>
<!-- alrdy-animate stylesheet (FOUC guard + helper classes) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/alrdy-animate@8.0.0-alpha.0/dist/alrdy-animate.css">
<!-- GSAP core + plugins (always load these) -->
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3/dist/ScrollTrigger.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3/dist/CustomEase.min.js"></script>
<!-- Add only the plugins your site actually uses -->
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3/dist/SplitText.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3/dist/Flip.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3/dist/Draggable.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3/dist/InertiaPlugin.min.js"></script>
<!-- Smooth scroll (omit to disable, or set smoothScroll: false in init) -->
<script defer src="https://cdn.jsdelivr.net/npm/lenis@1/dist/lenis.min.js"></script>
<!-- alrdy-animate last -->
<script defer src="https://cdn.jsdelivr.net/npm/alrdy-animate@8.0.0-alpha.0/dist/alrdy-animate.umd.cjs"></script>

Why defer — without it, every <script> blocks HTML parsing, which makes the whole page wait for each download in series. With defer, all scripts download in parallel while the parser sails through the body; they then execute in document order at the end of parsing. This is faster on every connection, and it’s a hard requirement for the load-fallback recipe to work on slow networks.

Tuning the two timeouts. FALLBACK_DELAY (default 1s) controls the hero staircase fade for aa-trigger="load" elements; tune it to taste — lower = faster fallback, higher = more time for JS to win the race. TIMEOUT_DELAY (default 4s) is the universal safety net: if init() hasn’t run by then, all [aa-animate] elements get revealed at their natural state with no entrance animation, so the page doesn’t stay invisible if the bundle is blocked or errors. Keep it well above FALLBACK_DELAY. When init eventually arrives it detects aa-timeout and skips its appearance-feature work to avoid rewinding the now-visible elements through their from-states — interactive components (tabs, slider, modal, nav, hover, cursor) still wire up normally.

Trim the plugin list. init({ debug: true }) prints any “Missing GSAP plugins” warnings with the exact <script> tag you need to add. Drop the lines you don’t use to keep the page lighter.

Paste this into Project Settings → Custom Code → Footer Code. The DOMContentLoaded wrapper is required because inline <script> tags in <body> execute during parsing, before deferred head scripts have run — without the wrapper, AlrdyAnimate would be undefined.

<script>
document.addEventListener('DOMContentLoaded', () => {
AlrdyAnimate.init({
debug: false, // flip to true while building to see plugin warnings
duration: 0.6, // global default; per-element aa-duration overrides
ease: 'smooth', // named ease; CustomEase plugin required
smoothScroll: true, // creates Lenis if window.Lenis is loaded
})
})
</script>

If you have a custom GSAP timeline that should run on every page (a navigation scroll-effect, a custom cursor, a marquee not handled by aa-marquee), append it to the same block. Wrap it in AlrdyAnimate.ready().then(...) so it runs after init() finishes — important when your code depends on Lenis being mounted, named eases (smooth, osmo, bounce, …) being registered, or feature elements being scanned.

<script>
document.addEventListener('DOMContentLoaded', () => {
AlrdyAnimate.init({ /* ... */ })
AlrdyAnimate.ready().then(() => {
// Example: hide the nav on scroll-down, show on scroll-up.
const nav = document.querySelector('.site-nav')
if (!nav) return
let lastY = window.scrollY
ScrollTrigger.create({
start: 0,
end: 'max',
onUpdate: (self) => {
const y = self.scroll()
gsap.to(nav, { yPercent: y > lastY && y > 80 ? -100 : 0, duration: 0.3 })
lastY = y
},
})
})
})
</script>

Reading the lib’s resolved options (motion preference, viewport class, default duration/ease) is a one-liner after ready — no need to re-detect:

AlrdyAnimate.ready().then(() => {
const { reducedMotion, optimizeMobile, breakpoints, duration, ease } = AlrdyAnimate.options
// use these to keep your custom GSAP timing in sync with the lib
})

If your global code doesn’t depend on alrdy-animate at all (a pure GSAP scene that targets your own classes), you don’t need ready() — you can put the GSAP calls directly inside the DOMContentLoaded callback. Both gsap and ScrollTrigger are already loaded by that point.

For animations that only run on a single page (a hero parallax, a custom scroll-driven scene), use Page Settings → Custom Code → Before </body> tag. Same pattern — wait for DOMContentLoaded, then AlrdyAnimate.ready().

<script>
document.addEventListener('DOMContentLoaded', () => {
AlrdyAnimate.ready().then(() => {
// Example: a scroll-driven hero scene specific to this page.
gsap.to('.hero__photo', {
scale: 1.15,
ease: 'none',
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom top',
scrub: 0.5,
},
})
})
})
</script>

AlrdyAnimate.ready() is safe to call from any page, even ones where you forgot to add the site-wide footer block — it resolves immediately if init() hasn’t been called. No defensive fallback needed.

The lib’s CSS file ships a single rule:

[aa-animate]:not([aa-ready]) { visibility: hidden; }

init() flips aa-ready on every animated element once their from-states are applied. Without this rule, every animated element would briefly render in its final state before init() runs, then snap to its from-state — visible flicker. The same rule is duplicated inline in the head snippet above so it’s active on the very first paint, before the lib’s CSS file has finished downloading on slow networks.

  1. Publish the site to a webflow.io staging URL.
  2. Open DevTools → Console. With debug: true you should see one log line: [alrdy-animate] initialized. Features: scroll, text; Plugins: ScrollTrigger, SplitText; Elements: 12; SmoothScroll: lenis
  3. If you see a “Missing GSAP plugins” warning, copy the suggested <script> tag into the head and republish.
  4. To check the slow-network fallback: DevTools → Network → throttle to “Slow 4G” → reload. Hero elements should fade up via the inline keyframe within ~1s, even before the lib bundle finishes loading.

Using with AI assistants (Claude Code, Cursor, etc.)

Section titled “Using with AI assistants (Claude Code, Cursor, etc.)”

If you author Webflow custom code with an AI assistant — generating embedded blocks, writing component-level GSAP, or reasoning about aa-* attribute combinations — point the assistant at the agent reference shipped inside the npm package.

The simplest path is to clone the alrdy-animate repo locally and reference its AGENTS.md from your project (or any AI-instruction file the tool reads). For Webflow workflows where you don’t have a local npm project, copy the contents of AGENTS.md from the GitHub repo into your assistant’s context once at the start of a session. It covers the attribute syntax conventions, the trigger system (including container inference and reverse pairing), every feature’s required GSAP plugins, and the init/destroy lifecycle — everything an assistant needs to suggest correct attribute combinations without guessing.

This guide assumes no page-transition library. Webflow’s default click-navigation behaviour means each page is a fresh document load, and init() runs once per page. If you want SPA-style transitions, use Barba.js and follow the Webflow + Barba guideinit / destroy / keepGlobals are designed for it.