Jump to content
Search Community

Strange scrollTrigger behaviour when using matchMedia

Vivodionisio test
Moderator Tag

Recommended Posts

Hi, 
 

I'm having some trouble understanding why my scrollTrigger isn't working properly when resizing the window (crossing from a max-width of 820 to a min-width of 821). The scrollTrigger animation should move the 'stats' (a, b, c and d) boxes from yPercent of 100 to 0.   

I have put together a minimal demo using the Svelte REPL, and incase it's part of the issue I've included 'testimonial-cards' which have a scrollTrigger applied, pinning their parent (from a min-width of 821).

https://svelte.dev/repl/748d7e8c003540899795fecb9eb690b8?version=3.59.1

 

Thanks in advance for any help or advice :)

 

UPDATE: Turns out the testimonial cards and associated scrollTrigger have no effect on the behaviour of the stats cards, so I've been able to make an even more minimal demo https://svelte.dev/repl/18f36c2136ce4a369dbc8e05f938413f?version=3.59.1

 

 

Edited by Vivodionisio
Realised I could minimise the demo further
Link to comment
Share on other sites

Hi,

First IDK if the CDN links are useful or not since you're importing GSAP and ScrollTrigger in your script section, but definitely that particular version (3.6) didn't had MatchMedia built into it. MatchMedia was added in version 3.11.

 

The issue I see is that both code blocks are doing the exact same thing. Here is the small screen code:

gsap.set('.stats-card', { yPercent: 100 });

gsap.to('.stats-card', {
  scrollTrigger: {
    trigger: '.stats-card',
    start: 'top 98%',
    end: 'top 75%',
    scrub: 3,
    markers: true,
    id: "small",
  },
  yPercent: 0,
  stagger: {
    each: 0.11
  }
});

So before the animation is created you're setting the position of the cards to be 100% on the Y axis, then they are animated with a stagger to yPercent: 0.

 

This is the code in your large screen:

gsap.from('.stats-card', {
  scrollTrigger: {
    trigger: '.stats',
    start: 'top 70%',
    end: 'top 75%',
    scrub: 3,
    markers: true,
    id: "large",
  },
  yPercent: () => 100,
  stagger: {
    each: 0.11
  }
});

Here you use a from instance that applies the styles you pass in the config object and then animates the elements to their natural position. So in this case the elements are animated from y 100% to 0%, just like the previous animation. So you do have different GSAP instances, but they're doing the same. See the problem?

 

Hopefully this clear things up.

Happy Tweening!

Link to comment
Share on other sites

Hi Rodrigo,

Thank you so much. Yes, I see what I did there. I was experimenting with an alternative animation for mobile and when I decided to go with the original it seems I merely reverted the values, ending up with two instances. Removing that duplicate fixed things in the REPL but I'm still having the issue after updating my component. Something else going on. 

I've created another demo, adding in a preceding testimonials section which is pinned with a scrollTrigger that scrubs through animating cards from point a to point b. I haven't manage to reproduce the same behaviour exactly, but there is an issue, which is perhaps the same cause, or part of it. When resizing the window to a min-width of  821px, the start and end values appear not be getting recalculated so that they end up in the wrong position. I tried using a static ScrollTrigger.refresh() in the match media callback (now removed) , but I think this should be happening automatically when a window is resized? So I'm foxed. Not sure what's happening here.

 

Here's the demo: 

https://svelte.dev/repl/748d7e8c003540899795fecb9eb690b8?version=3.59.1

  

Link to comment
Share on other sites

Hi,

 

The problem seems to stem from the fact that the fromTo() instance you're creating for the cards is created just once when the component is mounted. Then, depending on the screen size, you create or revert another ScrollTrigger instance. The issue here becomes order of operation. So when the screen goes above 821px a new ScrollTrigger instance is added. That happens before the stat cards but is added after to the ScrollTrigger instances, so ScrollTrigger is doing the proper calculations but the order is wrong.

 

You have two options. Create another MatchMedia add method and run the code for both instances above the 820px width:

mm.add('(min-width: 821px)',() => {			

  // -- Testimonial Cards -- // 
  let base = 140;
  const cards = gsap.utils.toArray('.testimonial-card');
  for (let i = 0; i < cards.length; i++) {
    const total = i * base;
    gsap.set(cards[i], { yPercent: total });
  }

  const tl = gsap.timeline({
    scrollTrigger: {
      trigger: '.what-parents-say',
      start: () => 'top top',
      end: () => '80%', // This is how long we pin
      pin: true,
      markers: { indent: 200 },
      scrub: true,
      anticipatePin: 0.2
    }
  });

  for (let i = 0; i < cards.length; i++) {
    if (i != 0)
      tl.to(
        cards[i],
        {
          yPercent: () => 0,
          duration: i * 10,
          ease: 'linear',
          boxShadow: '0px 0px 40px -14px rgba(0, 0, 0, 0.0)'
        },
        '<'
      );
  }
  gsap.fromTo('.stats-card',  { yPercent: () => 100 },{
    scrollTrigger: {
      trigger: '.stats',
      start: 'top 70%',
      end: 'top 45%',
      scrub: 3,
      markers: true,
      invalidateonrefresh: true,
    },
    yPercent: () => 0,
    stagger: {
      each: 0.11
    }
  });
}, section);

mm.add("(max-width: 820px)", () => {
  gsap.fromTo('.stats-card',  { yPercent: () => 100 },{
    scrollTrigger: {
      trigger: '.stats',
      start: 'top 70%',
      end: 'top 45%',
      scrub: 3,
      markers: true,
      invalidateonrefresh: true,
    },
    yPercent: () => 0,
    stagger: {
      each: 0.11
    }
  });
}, section);

Or add a lower refresh priority to the stat cards ScrollTrigger and a higher one to the one created inside the add() method:

mm.add(
  '(min-width: 821px)',
  () => {			

    // -- Testimonial Cards -- // 
    let base = 140;
    const cards = gsap.utils.toArray('.testimonial-card');
    for (let i = 0; i < cards.length; i++) {
      const total = i * base;
      gsap.set(cards[i], { yPercent: total });
    }

    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: '.what-parents-say',
        start: () => 'top top',
        end: () => '80%', // This is how long we pin
        pin: true,
        scrub: true,
        anticipatePin: 0.2,
        refreshPriority: 1,	
        markers: { indent: 200 },
      }
    });

    for (let i = 0; i < cards.length; i++) {
      if (i != 0)
        tl.to(
          cards[i],
          {
            yPercent: () => 0,
            duration: i * 10,
            ease: 'linear',
            boxShadow: '0px 0px 40px -14px rgba(0, 0, 0, 0.0)'
          },
          '<'
        );
    }
  },
  section
);
// -- Stat Cards -- //
gsap.fromTo('.stats-card',  { yPercent: () => 100 },{
  scrollTrigger: {
    trigger: '.stats',
    start: 'top 70%',
    end: 'top 45%',
    scrub: 3,
    markers: true,
    invalidateonrefresh: true,
    refreshPriority: 0,
  },
  yPercent: () => 0,
  stagger: {
    each: 0.11
  }
});

While the latter works as expected you could have another issue when this component is unmounted, since the fromTo() instance for the stats cards is not being reverted properly on cleanup. Since MatchMedia is a wrapper for GSAP Context you don't have to worry about that for the instance created inside MatchMedia. So you could wrap that particular instance in a GSAP Context instance in order to revert that as well:

const ctx = gsap.context(() => {
  // -- Stat Cards -- //
  gsap.fromTo('.stats-card',  { yPercent: () => 100 },{
    scrollTrigger: {
      trigger: '.stats',
      start: 'top 70%',
      end: 'top 45%',
      scrub: 3,
      markers: true,
      invalidateonrefresh: true,
      refreshPriority: 0,
    },
    yPercent: () => 0,
    stagger: {
      each: 0.11
    }
  });
}, section);

return () => {
  mm.revert();
  ctx.revert();
};

That seems to work the way you intend.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Solution found :D

The issue had to do with the order in which the scrollTriggers start and end values were being calculated and flow sections/divs on the page – stats come after testimonials.

 

GSAP recalculates start and end values of scrollTriggers automatically when the window is resized, but it does it in the order that the code is laid out. When the window grows to the larger screen width, values for Testimonials section are defined after those for Stats animation which have priority because they are outside of the mm.add()). These means that the new height (100vh) given to the testimonials section, move the Stats section relative to the start and end values defined before. 

I got around this firstly, by calling static ScrollTrigger.refresh() inside the mm.add()m after the Testimonials scrollTigger instance, and secondly, by defining the order that the scrollTrigger values should be re-calculated with the refreshPriority property. Awesome!

Here's a demo with solution:

https://svelte.dev/repl/0a34b8fb9ed2444b97613e904f9d74fe?version=3.59.1

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...