greensock / GSAP

GSAP (GreenSock Animation Platform), a JavaScript animation library for the modern web

Home Page:https://gsap.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Why is `scrollTop` set to zero when ScrollTrigger is initialized?

felipemarcos opened this issue · comments

This causes unintended effects if scroll-behavior is smooth. Links with anchors won't scroll to the right section and, instead, stay on top of the page due to line 276 (obj(0)).

GSAP/src/ScrollTrigger.js

Lines 273 to 278 in 6032c54

_scrollers.forEach(obj => {
if (_isFunction(obj)) {
obj.smooth && (obj.target.style.scrollBehavior = "auto"); // smooth scrolling interferes
obj(0);
}
});

Hi @felipemarcos

It's essential for ScrollTrigger to temporarily set the scroll position to the top when it does all of its start/end calculations because ScrollTriggers higher up can effect ones lower down on the page and everything must go top-down. For example, perhaps in the normal flow of the page, a particular trigger is 500px down but if there's another ScrollTrigger higher up that pins something for 300px, that would mean that should now start at 800px instead. And everything must get properly reverted (pinSpacers removed, animations rewound) to ensure that the cascade is honored and styles get applied properly.

It's a pretty complex operation to pull off all the magic that ScrollTrigger offers. So again, it's essential to temporarily force scrollTop to 0 when doing calculations before reverting to the previous scroll position (the user would never see that - it's just during the calculation phase).

It is NOT a good idea to apply scroll-behavior "smooth" because that would interfere with that behavior and all the calculations would be thrown off. In other words, ScrollTrigger would say "go to scrollTop of 0 so I can do these calculations..." and the browser would say "NOPE, I'm gonna take my time doing that...I won't do it immediately". Doh! It's sorta like applying a CSS transition to an element whose properties are being animated by GSAP. When GSAP applies a style, the CSS transition would say "sorry, I won't do that yet...I'm gonna take my sweet time doing that instead..."

If you think there's a bug, please make sure you provide a minimal demo (like a CodePen or Stackblitz) that clearly illustrates the issue with as little code as possible. Thanks!

@jackdoyle thank you for the detailed explanation!

Here is a minimal demo that showcases the problem:
https://scrolltrigger-anchor-link-test.vercel.app/#footer

Clicking on the link above should've taken you straight to the footer, but right now it stays at scroll 0. Comment out the code at index.js and see that it works correctly.

Repository:
https://github.com/felipemarcos/scrolltrigger-anchor-link-test

Thanks for such a nice minimal demo!

It looks to me like the problem is that you aren't creating your ScrollTriggers until after the "DOMContentLoaded" event and right after that is when the browser updates the scroll position. Like I said, it's generally not a good idea to use scroll-behavior: smooth. If you remove that, you'll see that it works as expected. Or if you don't set up your ScrollTriggers inside a DOMContentLoaded event.

Better yet, you could add something like this:

window.addEventListener("load", () => {
  let urlHash = window.location.href.split("#")[1],
      scrollElem = document.querySelector("#" + urlHash);
  if (scrollElem) {
    gsap.to(window, { scrollTo: scrollElem }); // don't forget to load/register ScrollToPlugin
  }
});

(and don't forget to load/register ScrollToPlugin)

Does that clear things up?

@jackdoyle Thank you, it does clear things up.

I can't remove DOMContentLoaded from my project as all my scripts are deferred, and I initialize ScrollTrigger inline on the page.

I can't remove scroll-behavior: smooth as my users expect smooth scrolling on their website.

I am severely limited in my options, but your code snippet helped me come up with a good enough solution for my use case.

window.addEventListener("load", () => {
  if (!location.hash) return;
  const node = document.querySelector(location.hash);
  if (!node) return;

  // Safari fix: Scroll to the element after the next repaint.
  requestAnimationFrame(() => {
    node.scrollIntoView({
      behavior: "smooth",
    });
  });
});

Nice solution, and thanks for sharing.