DEV Community

Keymel Gaston
Keymel Gaston

Posted on

4 Motion Design Bugs That Break Astro + GSAP + Lenis Sites in Production

I got tired of debugging the same four things every time I built a motion-design landing page with Astro, GSAP, and Lenis — so I wrote them down. Here's each bug, what causes it, and how to fix it.

1. Scroll-triggered animations that never fire

How to spot it: you scroll to a section, and the animation that's supposed to play doesn't. No console error — the element just sits in its "before" state forever.

The cause: Lenis replaces native scrolling with a virtual, smoothed one. GSAP's ScrollTrigger has no built-in awareness that Lenis exists — it's still listening for native scroll behavior Lenis has effectively taken over.

The fix:

lenis.on("scroll", ScrollTrigger.update);

gsap.ticker.add((time) => {
  lenis.raf(time * 1000);
});

gsap.ticker.lagSmoothing(0);
Enter fullscreen mode Exit fullscreen mode

Do this once, at setup. Do it per-section instead and you'll eventually forget one — which is exactly how this bug ships to production.

2. Images that fade in as blank boxes

How to spot it: a smooth, well-eased fade-in animation... revealing nothing. The image pops in abruptly partway through, or right after the animation finishes.

The cause: the animation triggers on DOMContentLoaded, component mount, or scroll-into-view — none of which guarantee the image has actually finished downloading and decoding.

The fix:

async function waitForImage(img) {
  if (img.complete) {
    await img.decode?.().catch(() => {});
    return;
  }
  await new Promise((resolve) => {
    img.addEventListener("load", resolve, { once: true });
    img.addEventListener("error", resolve, { once: true });
  });
}
Enter fullscreen mode Exit fullscreen mode

Await this before starting the fade. One helper function, one fewer confusing bug report.

3. Buttons that stop responding for no visible reason

Two separate causes here — worth checking both.

3a — a decorative layer is eating the click.

An animated background shape or overlay ends up in a higher stacking context than an interactive element near it. The button looks normal. It just doesn't respond.

- z-index left to chance, decided per component
+ pointer-events: none on every purely decorative element, always
Enter fullscreen mode Exit fullscreen mode

3b — a React/Vue island hasn't hydrated yet.

With lazy hydration (client:visible, client:idle), there's a real window where an island's HTML is on the page but its listeners haven't attached. A click in that window isn't delayed — it's lost.

const [hydrated, setHydrated] = useState(false);
useEffect(() => setHydrated(true), []);

<button disabled={!hydrated}>
  {hydrated ? "Click me" : "Loading…"}
</button>
Enter fullscreen mode Exit fullscreen mode

Don't just hope the click lands after hydration — disable the element until hydration is confirmed. This race can pass casual manual testing and still fail reliably under real load — it's the kind of thing that's easy to miss until you have automated tests hitting it under real concurrency.

4. SEO and Lighthouse scores that don't hold up

The most common individual causes:

  • og:image set to a relative URL — silently fails the Open Graph spec, which requires an absolute URL
  • No base URL configured, so canonical links can't resolve
  • Missing alt text, skipped heading levels, no sitemap

The fix: centralize your <head> in one layout every page uses, resolve image and canonical URLs to absolute paths explicitly, and generate a sitemap.

One honest note on Lighthouse scores

If you're running Lighthouse on a site with real scroll-driven animation and you're not hitting a perfect 100 on Performance — that's expected. A 100 with heavy motion actually running is rare; it usually means the animation isn't doing much. A defensible 80s–90s score with genuine motion design running is more honest than a suspiciously perfect one.


I put all four of these — tested, with automated regression tests, demonstrated live rather than just described — into a full Astro + GSAP + Lenis landing page template. The live demo is here if you want to see them in action, and the kit itself is on Gumroad if you'd rather start from a base that already has this solved.

Happy to answer questions about any of these in the comments — especially the hydration race in #3, that one surprised me too.

Top comments (0)