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.

This goes into Project Settings → Custom Code → Head Code as two blocks: an inline safety snippet, then the library's CSS + scripts. The stylesheet is loaded non-blocking (off the critical render path), which is safe because the only render-critical rules — the FOUC guard and the slow-network fallback — are inlined in the first block.

Paste this first, above the CSS/scripts block. Inline (no request), so it works on the very first paint: the FOUC guard ([aa-animate]:not([aa-ready]) { visibility: hidden }) hides animated elements until init() reveals them, plus a net so nothing stays blank if the bundle is slow or never arrives, and a <noscript> reveal for JS-off. For aa-trigger="load" / "load-once" heroes it also adds the slow-network aa-fallback staircase that fades the hero in if the bundle is late.

<!-- alrdy-animate FOUC guard + slow-network/timeout safety. -->
<script>
const FALLBACK_DELAY = 100; // ms — load elements fade in via CSS keyframe
const TIMEOUT_DELAY = 4000; // ms — universal reveal if init never runs
const selectors = '[aa-trigger~="load"][aa-animate], [aa-trigger~="load-once"][aa-animate]';
const styled = new WeakSet();
const applyFallback = () => {
if (document.documentElement.hasAttribute('aa-loaded')) return;
document.documentElement.setAttribute('aa-fallback', '');
document.querySelectorAll(selectors).forEach((el) => {
if (styled.has(el)) return;
styled.add(el);
const baseDelay = parseFloat(el.getAttribute('aa-delay')) || 0;
if (baseDelay > 0) el.style.animationDelay = baseDelay + 's';
});
};
// Poll, not DOMContentLoaded — render-blocking scripts can delay it past the timers.
const deadline = performance.now() + FALLBACK_DELAY + 8000;
const tryFallback = () => {
if (document.documentElement.hasAttribute('aa-loaded')) return;
applyFallback();
if (document.readyState === 'loading' && performance.now() < deadline) {
setTimeout(tryFallback, 100);
}
};
setTimeout(tryFallback, FALLBACK_DELAY);
setTimeout(() => {
if (document.documentElement.hasAttribute('aa-loaded')) return;
document.documentElement.setAttribute('aa-timeout', '');
}, TIMEOUT_DELAY);
</script>
<style>
/* FOUC guard (excluding LCP) */
[aa-animate]:not([aa-ready]):not([aa-trigger~="lcp"]) { visibility: hidden; }
[aa-animate][aa-trigger~="lcp"]:not([aa-ready]) { visibility: visible; opacity: 0.01; }
/* Fallback handling */
@keyframes aa-fallback-appear {
from { opacity: 0; }
to { opacity: 1; }
}
html[aa-fallback] [aa-trigger~="load"][aa-animate]:not([aa-ready]),
html[aa-fallback] [aa-trigger~="load-once"][aa-animate]:not([aa-ready]) {
visibility: visible;
animation: aa-fallback-appear 0.5s ease both;
}
html[aa-fallback] [aa-animate][aa-trigger~="lcp"]:not([aa-ready]) { opacity: 1; transition: opacity 0.5s ease; }
/* Timeout handling */
html[aa-timeout] [aa-animate]:not([aa-ready]) { visibility: visible; }
html[aa-timeout] [aa-modal-group] [aa-animate]:not([aa-ready]) { visibility: hidden; } /* Keep modals hidden */
html[aa-timeout] [aa-animate][aa-trigger~="lcp"]:not([aa-ready]) { opacity: 1; }
</style>
<noscript><style>[aa-animate]{visibility:visible!important;opacity:1!important;transform:none!important;filter:none!important;clip-path:none!important}</style></noscript>

Tuning the timeouts. FALLBACK_DELAY (100ms) — how long before the hero fades in via CSS when the bundle is slow; at 100ms the CSS fade usually drives the entrance (a cold bundle rarely beats it), so raise it toward 1s to give GSAP first shot instead. TIMEOUT_DELAY (4s) — the universal net: reveal every [aa-animate] at its natural state if init() never runs; keep it well above FALLBACK_DELAY. When init() arrives it detects aa-timeout and skips appearance work (no rewind) while still wiring interactive components.

Preload the hero webfont. Independent of the fallback, but the same first-paint win — GSAP text splits measure the rendered font and the hero is usually your LCP element, so a late font swap reflows the split and delays LCP. Add this near the top of the head with your real font URL (DevTools → Network → Font, or the site CSS — cdn.prod.website-files.com/<site-id>/<asset>.woff2):

<link rel="preload" as="font" type="font/woff2" crossorigin
href="https://cdn.prod.website-files.com/YOUR_SITE_ID/YOUR_FONT.woff2">

Paste this second, below the safety snippet. Pick the tab that matches where GSAP comes from — your choice is remembered as you move between pages.

The library registers GSAP plugins itself when init() runs, so these <script> tags are load-order-independent: list them in any order, and they work whether GSAP arrives from the CDN or from Webflow's own build.

Self-contained: alrdy-animate's own GSAP 3 build, Lenis, and the library, all downloaded in parallel via defer. Use this unless your Webflow site already loads GSAP for its native interactions — shipping GSAP twice is wasted weight.

<!-- alrdy-animate stylesheet — non-blocking (critical rules are inlined above) -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/alrdy-animate@8.1.1/dist/alrdy-animate.css"
media="print" onload="this.media='all';this.onload=null">
<noscript>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/alrdy-animate@8.1.1/dist/alrdy-animate.css">
</noscript>
<!-- 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.1.1/dist/alrdy-animate.umd.js"></script>

A few notes:

  • defer lets all scripts download in parallel and run in order after parsing — faster, and init() runs sooner. Plugins are registered at init(), so order doesn't matter and the inline snippet doesn't depend on it.
  • Trim the plugins. init({ debug: true }) logs any "Missing GSAP plugins" warning with the exact <script> to add — drop the lines you don't use.
  • Non-blocking stylesheet. media="print" + onload="this.media='all'" keeps the CSS off the critical path (the <noscript> covers JS-off). Safe because every first-paint-critical rule is in the inline snippet above. Caveat: an above-the-fold marquee/tabs/hover-text component can briefly flash unstyled before the file lands — if that matters, inline the whole dist/alrdy-animate.css instead.

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({
duration: 0.6,
ease: 'smooth',
presets:{
'heading-style-h2': {
animate: 'text-fade-10',
split: 'words',
stagger: '0.05',
},
'heading-style-h3': {
animate: 'text-fade',
split: 'lines-chars',
stagger: '0.01 0.1',
},
}
})
})
</script>

Beyond the aa-* attributes, you can write your own GSAP timelines and run them alongside the library. The pattern is the same in both cases below — wait for DOMContentLoaded, then, if your code depends on the library, chain AlrdyAnimate.ready() so it runs after init() has mounted Lenis, registered the named eases (smooth, osmo, bounce, …), and scanned the feature elements. Only where you paste it differs.

AlrdyAnimate.ready() is safe to call anywhere — even on a page where you forgot the footer init() block it resolves immediately rather than hanging, so no defensive fallback is needed. And if your timeline only targets your own classes and never touches the library, you can skip ready() and call gsap / ScrollTrigger directly in the DOMContentLoaded callback — both are already loaded by then.

For a custom timeline that should run everywhere — a nav scroll-effect, a custom cursor, a marquee not handled by aa-marquee — append it to the Footer Code block, after the init() call:

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

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. No init() here — that already ran from the site-wide footer — just 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>
  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 in via the inline keyframe within ~100ms, 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.