Slow-network + init-timeout fallback
The lib hides every [aa-animate] element with visibility: hidden until init() runs. On a slow connection (or if the bundle is blocked, errors, or is misconfigured), that leaves animated content invisible. This snippet adds two layers of safety:
| Layer | Default | What it does |
|---|---|---|
aa-fallback | 1s | Fades aa-trigger="load" elements (hero copy) via a CSS keyframe — graceful staircase, respecting aa-delay and aa-stagger. |
aa-timeout | 4s | Reveals every [aa-animate] element at its natural state with no animation — universal safety net if init never runs. |
Drop this snippet into <head> above the alrdy-animate <script> and <link> tags:
<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) { // Parent is a container (e.g. card grid) — show it without fading, // animate each direct child with the same stagger the JS init applies. 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 the body hasn't been parsed yet (slow connection blocking on // render-blocking <script> tags), defer to DOMContentLoaded so the // elements actually exist when we querySelectorAll them. 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; /* `both` so the from-state holds during any pre-start delay too. With `forwards`, a delayed element renders at its natural opacity during the delay, then jumps to opacity 0 at start — visible flicker. */ animation: aa-fallback-appear 0.5s ease both; } html[aa-timeout] [aa-animate]:not([aa-ready]) { visibility: visible; }</style>Tune the two constants at the top to taste. Keep TIMEOUT_DELAY well above FALLBACK_DELAY — the first is “let JS win the race for the hero”, the second is “give up, show the page”.
Why the inline <style> — the same FOUC + fallback rules ship in dist/alrdy-animate.css, but on a slow connection that file lands after the HTML renders. Without inlining, every animated element flashes briefly visible, then disappears when the lib’s CSS arrives, then fades back in when the timeout fires. Inlining the critical CSS in <head> closes that window — the FOUC guard is active from the very first paint.
How the staircase works
Section titled “How the staircase works”The script mirrors the two timing attributes the JS init already understands, so the fallback reveal looks like a quieter version of the real animation:
aa-delay="<seconds>"— applies a delay to the element’s own fade-up. E.g. a paragraph withaa-delay="0.2"appears 200ms after the headline.aa-stagger="<seconds>"on a parent (with children) — the parent itself shows immediately (no fade), and each direct child fades up ati * stagger, just like the JS path animates the children with stagger when they’re inside anaa-staggerparent. If the parent also hasaa-delay, that’s added as a base offset to every child.
CSS alone can’t read aa-delay or expand aa-stagger across non-sibling children — :nth-child only works among siblings, CSS counters can’t drive animation-delay, and typed attr() isn’t widely supported yet. The 18-line script handles both rules in one pass.
What each layer does
Section titled “What each layer does”init() sets aa-loaded on <html> as its first synchronous step. The two timers check for that attribute and bail out if it’s already set.
Layer 1 — aa-fallback (1s default)
Section titled “Layer 1 — aa-fallback (1s default)”Sets html[aa-fallback]. The CSS rule (matching [aa-trigger~="load"], so it covers both aa-trigger="load" and combined forms like aa-trigger="load event:enter") fades those elements up via a keyframe animation, respecting aa-delay and aa-stagger so the staircase still plays.
When the bundle eventually arrives, the lib’s load-trigger branches detect aa-fallback and skip their JS animation entirely — the elements stay where the fallback put them, no replay or flash. init() clears aa-fallback at the end, so subsequent Barba navigations don’t re-trigger the keyframe.
Scoped to [aa-trigger~="load"] on purpose: scroll-triggered elements below the fold stay hidden until the lib actually arrives. Only elements the author has explicitly marked as “needs to be visible immediately” are eligible for the fallback fade.
Layer 2 — aa-timeout (4s default)
Section titled “Layer 2 — aa-timeout (4s default)”Sets html[aa-timeout]. The CSS rule reveals every [aa-animate] element at its natural rendered state — no animation, no from-state. This is the deep-fail safety net: if JS errored, the bundle is blocked, or the network is so slow that even the hero staircase isn’t enough, the page still ends up usable.
When init() eventually arrives, it detects aa-timeout and switches to bypass mode:
- Drops
scroll,text,reveal,parallaxfrom the feature load set — no from-states applied, no entrance ScrollTriggers, no flash from rewinding the now-visible elements. - Skips the reduced-motion fade pass.
- Still wires up interactive features (tabs, slider, marquee, modal, nav, hover, cursor, split) so components remain functional.
- Still mounts smooth-scroll, scroll-state, and scroll-target globals.
- Flips
aa-readyon every[aa-animate]element so the DOM ends in a consistent state. - Clears
aa-timeout, so the next Barba navigation runs animations normally.
The tradeoff: appearance animations don’t play on this load (the user already saw the content for 4+ seconds), and event-triggered scroll/text/reveal entries don’t fire on this page. At that point, page consistency outweighs animation correctness.
Customising the motion
Section titled “Customising the motion”The default keyframe is a small upward translate (translateY(1.5rem) → 0) plus opacity. To use a different fallback motion (slide-down, pure fade, scale-in), override the keyframe in your own CSS after the inline snippet:
@keyframes aa-fallback-appear { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); }}The duration, easing, and selector stay the same — just the keyframe content changes.
Use defer on your CDN scripts
Section titled “Use defer on your CDN scripts”For the per-element delays to actually fire on slow networks, the body needs to be parsed by the time the timer fires. Add defer to every <script src="…"> in <head>:
<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/lenis@1/dist/lenis.min.js"></script><script defer src="https://cdn.jsdelivr.net/npm/alrdy-animate@8.0.0-alpha.0/dist/alrdy-animate.umd.cjs"></script>Without defer, every <script> blocks HTML parsing — on slow 4G the body hasn’t been parsed by the time the 1s timer fires, so querySelectorAll returns no elements and the per-element delays never get applied. The snippet’s built-in DOMContentLoaded guard catches this case as a safety net (it defers the apply step until the DOM is ready), but by then the lib bundle has usually finished loading too and init() runs first — meaning the fallback never fires at all on slow networks.
defer flips this: the parser sails through the body without waiting for downloads. The [aa-trigger~="load"] elements exist by the time the timer fires, the snippet applies their delays inline, and the CSS keyframe plays out the staircase before the lib bundle even finishes downloading.
When you don’t need this
Section titled “When you don’t need this”You can skip the snippet if any of these are true:
- The bundle ships in your own JS bundle (Vite / Next.js / Webpack), not from a CDN. In that case bundle availability is bound to page render, not network latency, and a JS error would prevent the React tree itself from mounting — there’s no
aa-animateflash to protect against. - You’re shipping behind aggressive caching where most visitors hit a warm bundle and you’re comfortable with elements staying invisible on the rare error case.
If you only want one of the two layers, delete the other setTimeout block and its corresponding CSS rule.
Page transitions (Barba)
Section titled “Page transitions (Barba)”This snippet only protects the first page load. On subsequent navigations the bundle is cached and init() runs in beforeEnter parallel to the leave timeline (see the Webflow + Barba recipe). aa-loaded persists across navigations, so the fallback timer’s check passes immediately and nothing fires.
Verifying it locally
Section titled “Verifying it locally”Layer 1 — aa-fallback (slow network)
Section titled “Layer 1 — aa-fallback (slow network)”- Confirm your CDN
<script>tags usedefer(see above) — without it the body isn’t parsed when the timer fires and the per-element delays can’t apply. - Open DevTools → Network → check “Disable cache” → throttle to “Slow 3G” (or “Slow 4G”).
- Reload. You should see:
- Hero elements stay invisible for ~1s (the
FALLBACK_DELAYinterval). - At the timer mark, they fade up — respecting any
aa-delayandaa-staggeryou set, so the staircase plays out before the bundle finishes downloading. - When the bundle eventually lands, no second animation plays — the elements stay where the fallback put them.
- Hero elements stay invisible for ~1s (the
- Open the Elements panel and inspect a load-trigger element while the fallback is in flight. You should see
style="animation-delay: 0.2s"(orstyle="animation: aa-fallback-appear 0.5s ease 0.1s 1 normal both running"on a stagger child). If those inline styles are missing, the body wasn’t parsed when the timer fired — check yourdeferattributes.
Layer 2 — aa-timeout (bundle never arrives)
Section titled “Layer 2 — aa-timeout (bundle never arrives)”- In DevTools → Network, block the alrdy-animate
<script>URL (right-click the request → Block request URL). - Reload. You should see:
- All
[aa-animate]elements stay hidden for ~4s (theTIMEOUT_DELAYinterval). - At the timer mark, every animated element on the page becomes visible at its natural rendered state — no fades, no entrance motion.
- The Elements panel shows
<html aa-timeout>.
- All
- Unblock the request and reload. Hero fades in via Layer 1 if it gets stuck under 4s, otherwise normal init runs and the snippet does nothing.