Search code examples
javascripthtmljquerygsapscrolltrigger

Animating child elements in ScrollTrigger GSAP horizontal scroll


I have an svg which forms the basis of my horizontal scroller.

Within this svg, I have added the class .animate to the elements which I want to fade in up as the item comes into view. The .animate class for reference has been added to all the text items in the svg.

Currently, only the .animate elements that are in view initially fade in up. When I scroll down to continue the scroller, the other elements are static. They're not fading in or translating up or down in any way?

TL;DR, here is what I'm trying to achieve:

  • When the scroller pins in place, and the user continued to scroll down, start fading away .horizontalScroller__intro.
  • Once .horizontalScroller__intro has faded away, start the horizontal scroll for .horizontalScroller__items
  • Any elements with the class of .animate in my scroller will fade in up to its original position.

Note: I understand SO rules and preferences to post code here. But, my demo's contain a length SVG, which I cannot post here as it exceeds SO's character limit.

Here is a demo of my latest approach

From the scrollTrigger docs, containerAnimation is what helps achieve animations on horizontal scrollers, and is what I've tried to achieve.

However, in my demo above, I have the following issues:

  1. .horizontalScroller__intro doesn't show initially, when it should, and should fade out on scroll.
  2. The horizontal scroller doesn't work anymore
  3. The .animate elements that are in view, do not fade in up

If I use timeline (see below snippet), then the intro fade out and scroller works. But, doesn't animate in the child elements, which is where I need containerAnimation

$(function() {

  let container = document.querySelector(".horizontalScroller__items");

  let tl = gsap.timeline({
    scrollTrigger: {
      trigger: ".horizontalScroller",
      pin: ".horizontalScroller",
      anticipatePin: 1,
      scrub: true,
      invalidateOnRefresh: true,
      refreshPriority: 1,
      end: '+=4000px',
      markers: true,
    }
  });

  tl.to('.horizontalScroller__intro', {
    opacity: 0,
  })

  tl.to(container, {
    x: () => -(container.scrollWidth - document.documentElement.clientWidth) + "px",
    ease: "none",
  })

});

I'm struggling to find a way in which I can make the intro fade in, the scroller scroll horizontally, and the .animate elements to fade in, or fade in up.

Edit:

@onkar ruikar, see notes based on your sandbox below:

  1. When you scroll down and the comes into view, I want the initial .animate elements to scroll up into view (currently, once the text fade away, and then the horizontal scroller starts working, only then does the .animate that are suppose to be in view, fade in up
  2. After the initial .animate elements have loaded, the next .animate elements that are part of the scroller, they do not fade in up. They're static. As each .animate element comes into view, then it should fade in up (I think it's currently triggering once, for all the elements).

See visual here:

enter image description here

In the above gif, you can see the first two text blocks are hidden, as soon as they're in view, I want them to fade up. Then the 3rd and 4th text blocks are static, when they should fade up as the user scrolls to that section.


Solution

  • You need to use onUpdate method on the scroll trigger.

    onUpdate: self => console.log("progress", self.progress)
    

    Based on the self.progress set opacity, x position etc.

    Full demo on codesandbox. Click on "Open Sandbox" button on bottom right to see the code.

    if ("scrollRestoration" in history) {
      history.scrollRestoration = "manual";
    }
    $(function() {
      let container = document.querySelector(".horizontalScroller__items");
      let elements = gsap.utils.toArray(
        document.querySelectorAll(".animate")
      );
      let intro = document.querySelector(".horizontalScroller__intro");
      let svg = document.querySelector("svg");
      let animDone = false;
      window.scrollPercent = -1;
    
      var scrollTween = gsap.to(container, {
        ease: "none",
        scrollTrigger: {
          trigger: ".horizontalScroller",
          pin: ".horizontalScroller",
          anticipatePin: 1,
          scrub: true,
          invalidateOnRefresh: true,
          refreshPriority: 1,
          end: "+=600%",
          markers: true,
          onEnter: (self) => {
            moveAnimate();
          },
          onLeaveBack: (self) => {
            resetAnimate();
          },
          onUpdate: (self) => {
            let p = self.progress;
            if (p <= 0.25) {
              let op = 1 - p / 0.23;
              intro.style.opacity = op;
              animDone = false;
            }
    
            if (p > 0.23) {
              moveAnimate();
              // we do not want to shift the svg by 100% to left
              // want to shift it only by 100% - browser width
              let scrollPercent =
                (1 - window.innerWidth / svg.scrollWidth) * 100;
              let shift = ((p - 0.22) * scrollPercent) / 0.78;
              gsap.to(svg, {
                xPercent: -shift
              });
            }
          }
        }
      });
    
      function resetAnimate() {
        gsap.set(".animate", {
          y: 150,
          opacity: 0
        });
      }
      resetAnimate();
    
      function moveAnimate() {
        for (let e of elements) {
          if (ScrollTrigger.isInViewport(e, 0.4, true))
            gsap.to(e, {
              y: 0,
              opacity: 1,
              duration: 2
            });
        }
      }
    });
    

    You need to set opacity 0 on .animate elements in CSS. And use end: '+=400%' instead of 4000px. Relative dimensions can be used in position based calculations easily.