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 | 100ms | Fades aa-trigger="load" / aa-trigger="load-once" 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. Closed modals are excluded (their natural state is hidden). |
Drop this snippet into <head> above the alrdy-animate <script> and <link> tags — and ideally above any other render-blocking <script> (e.g. Typekit), so the two timers start counting from page load rather than after that script downloads:
<!-- 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>Tune the two constants up top. At FALLBACK_DELAY = 100ms the CSS fade usually drives the hero entrance (a cold bundle rarely beats 100ms) — smooth and bundle-independent; raise it toward 1000ms to give GSAP first shot at the entrance instead. Keep TIMEOUT_DELAY well above it — that's the "give up and show the whole page" net. The script reads each element's aa-delay and sets animation-delay inline so the staircase still reads; it ignores aa-stagger (the split units don't exist pre-bundle, so it fades each element as a whole).
The FOUC guard excludes lcp (it paints on purpose at the 0.01 floor), so the lcp floor + reveal rules live in this same <head> block.
Why inline — the FOUC guard + lcp floor also ship in dist/alrdy-animate.css, but that file loads after first paint (non-blocking), so without an inline guard every animated element flashes visible, then hides when the guard lands, then reveals at init(). The fallback keyframe + aa-fallback / aa-timeout rules ship only here (nothing else sets those attributes), so the snippet is their single source — no cross-origin dist copy to collide with.
Preload the hero webfont
Section titled “Preload the hero webfont”Independent of the fallback, but the same kind of first-paint win: preload the font your hero text uses so it's available on the first frame. GSAP text effects (line / char splits) measure the rendered font, and the hero is usually your LCP element — a late font swap reflows the split and pushes out LCP. Add this near the top of <head> with your real font URL (Webflow: DevTools → Network → Font, or the site's 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">What each layer does
Section titled “What each layer does”init() sets aa-loaded on <html> as its first step; both timers bail if it's already set.
aa-fallback(1s) — fadesload/load-onceelements up via the keyframe (respectingaa-delay). Scoped to those triggers, so below-the-fold scroll elements stay hidden until the lib arrives. When the bundle lands, the load branches detectaa-fallbackand skip their JS animation (no replay/flash);init()clears the attribute.aa-timeout(4s) — the deep-fail net: reveals every[aa-animate]at its natural state, no animation. Closed modals stay hidden (a modal card'saa-animateis its open entrance — thehtml[aa-timeout] [aa-modal-group]rule re-hides them). Wheninit()arrives underaa-timeoutit skips appearance features (no rewind from-states) but still wires interactive components + globals, then clears the attribute. Tradeoff: entrance animations don't play on that load — the user already saw the content for 4s+.
Customising the motion
Section titled “Customising the motion”The default keyframe is a plain opacity fade. To use a different fallback motion (fade-up, slide-down, scale-in), edit @keyframes aa-fallback-appear directly in the snippet — it's the only definition, so there's nothing to override:
@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 edit the keyframe.
Why it polls (don't gate on DOMContentLoaded)
Section titled “Why it polls (don't gate on DOMContentLoaded)”A render-blocking <script> (Typekit, or Webflow's jQuery/GSAP bundle) keeps document.readyState at 'loading' and delays DOMContentLoaded — on a slow connection, past TIMEOUT_DELAY. So a DOMContentLoaded gate would never run the staircase. The snippet instead polls for the hero (which parses early, near the top of <body>) and applies the moment it appears.
Still add defer to the CDN <script> tags you control (they download in parallel, init() runs sooner — you can't defer Webflow's injected scripts), and load alrdy-animate's GSAP build instead of Webflow's native GSAP, not both.
When you don't need this
Section titled “When you don't need this”The fallback only matters when something can leave the FOUC guard's visibility: hidden in place — i.e. whenever init() can be slow or fail to run. You can skip the snippet if:
- You don't use the FOUC guard at all — you don't load
alrdy-animate/style(or otherwise apply the[aa-animate] { visibility: hidden }rule), and you accept a brief reflow asinit()applies from-states. With nothing hiding the elements, there's nothing to reveal. - The bundle is inlined into the initial HTML payload (not a separate
<script src>/ chunk) and you're comfortable with elements staying invisible in the rare caseinit()throws.
"It's a Vite / Next.js / Webpack app, not a CDN script" is not, on its own, a reason to skip it — see Using it in a bundled app below. A separately-loaded bundle is still subject to download latency and chunk-load failures, both of which leave guarded elements hidden.
If you only want one of the two layers, delete the other layer's setTimeout(...) (for aa-fallback, the tryFallback block and its scheduling setTimeout) and its corresponding CSS rule.
Using it in a bundled app
Section titled “Using it in a bundled app”The moment you load alrdy-animate/style in a Next.js / Vite / Webpack app, the same two risks apply: the FOUC guard hides every [aa-animate] element until init() runs, and init() waits on the JS bundle — which a slow connection delays and a failed chunk-load (or an error inside init() itself) can prevent outright, leaving the SSR'd hero invisible while the rest of the page renders. So you want the same two layers.
One rule that's easy to get wrong: the timer must run independently of your app bundle. Put it in a useEffect or a top-level module import and it can't fire until the bundle loads — which is the very thing it's meant to beat. It has to be inline in the document <head>, ahead of (or beforeInteractive relative to) your app's JS.
Use the same <script> + <style> + <noscript> as the snippet at the top of this page — only the placement and one trim change:
- The base FOUC guard and
lcpopacity floor already come fromimport 'alrdy-animate/style'(a render-blocking<link>, present at first paint), so you don't re-inline those two rules — only the fallback keyframe and thehtml[aa-fallback]/html[aa-timeout]reveal rules, which no longer ship indist. - The
html[aa-fallback]lcp rule out-specificities the imported0.01floor regardless of<link>order, so it still wins.
Next.js App Router — app/layout.tsx
Section titled “Next.js App Router — app/layout.tsx”import 'alrdy-animate/style' // FOUC guard + lcp floor (render-blocking, at first paint)
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> {/* Fallback keyframe + reveal rules — NOT in dist, so inline them here. */} <style dangerouslySetInnerHTML={{ __html: ` @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 .5s ease both } html[aa-timeout] [aa-animate]:not([aa-ready]) { visibility: visible } html[aa-timeout] [aa-modal-group] [aa-animate]:not([aa-ready]) { visibility: hidden } /* keep closed modals hidden */ html[aa-fallback] [aa-animate][aa-trigger~="lcp"]:not([aa-ready]) { opacity: 1; transition: opacity .5s ease } html[aa-timeout] [aa-animate][aa-trigger~="lcp"]:not([aa-ready]) { opacity: 1 } ` }} /> {/* The timer — paste the <script> body from the snippet at the top of this page. */} <script dangerouslySetInnerHTML={{ __html: `/* FALLBACK_DELAY / TIMEOUT_DELAY timer from the snippet above */` }} /> {/* JS-disabled reveal — same <noscript> as the snippet above. */} <noscript><style dangerouslySetInnerHTML={{ __html: `[aa-animate]{visibility:visible!important;opacity:1!important;transform:none!important;filter:none!important;clip-path:none!important}` }} /></noscript> </head> <body>{children}</body> </html> )}A raw <script dangerouslySetInnerHTML> in the <head> is injected into the SSR'd HTML and runs before hydration, so it beats the bundle without needing next/script — but <Script strategy="beforeInteractive"> from next/script works too. The timer only ever sets aa-* attributes on <html> and an inline animation-delay on plain DOM nodes — neither is a React-controlled prop, so there's no hydration mismatch.
Next.js Pages Router — pages/_document.tsx
Section titled “Next.js Pages Router — pages/_document.tsx”Put the same three tags inside <Head> in _document.tsx (using dangerouslySetInnerHTML for the <script> and <style>). Keep the import 'alrdy-animate/style' in _app.tsx.
Plain Vite / SPA
Section titled “Plain Vite / SPA”Paste the snippet verbatim into your index.html <head>, exactly like the Webflow block — there's no async-import subtlety, the inline <style> is the single source.
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”aa-fallback— DevTools → Network → "Disable cache" + throttle to Slow 3G, reload. The hero should fade in (<html aa-fallback>in the Elements panel) before the 4s timeout, and the bundle landing later should not replay it. If you only ever get the abrupt 4s reveal, you have an old copy that gated onDOMContentLoaded— re-copy the current (polling) snippet.aa-timeout— Network → block the alrdy-animate<script>URL, reload. Everything stays hidden ~4s, then reveals at its natural state (<html aa-timeout>).