Jump to content
Search Community

ScrollTrigger issue with Gatsby

Vlad Tw test
Moderator Tag

Go to solution Solved by OSUblake,

Recommended Posts

Hi guys! 

I come humbly in front of you with few drops of hope left, after 5 full days of switching between possible solutions to get a consistent ScrollTrigger behavior on a Gatsby site. Getting directly to you is my last resort, as every google and gsap forum link regarding ScrollTrigger and Gatsby is already visited. 😒

 

I cannot get a CodePen reproducing the exact issue so I'll try my best to describe it here.

 

Shortly, the problem seems to be, as I suspect, that the ScrollTrigger does not refresh itself when Javascript pops into the browser on top of the SSR-ed html/css bundle.

 

Here's what i did.

I created several projects with different versions for dependencies, but i will stick to the simplest one with all dependencies up to date.It's a gatsby with material-ui plugin added, who's exact structure can be found here: https://github.com/mui-org/material-ui/tree/master/examples/gatsby

There are no other plugins added, nor any other configs/plugins changed. 

 

I rendered the component that will contain the ScrollTrigger (AboutBlock) in the AboutPage page:

about.js

const AboutPage = () => {
    return (
      <AboutBlock />
    )
}
export default AboutPage

 

This is the component where i try to animate some elements on reveal when scrolled into view:

aboutBlock.js

import gsap from "gsap";
import ScrollTrigger from 'gsap/ScrollTrigger';
import animateReveal from "./gs_reveal";

export default function AboutBlock() {
      gsap.registerPlugin(ScrollTrigger)
  
      const revealRefs = useRef([])
      revealRefs.current = []
  
      useLayoutEffect(() => {
        let scrollTriggers = []

        scrollTriggers = animateReveal(revealRefs.current)
        return () => {
            scrollTriggers.forEach(t => t.kill(true))
        }
    }, []);
  
      const addToRevealRefs = el => {
        if (el && !revealRefs.current.includes(el)) {
            revealRefs.current.push(el);
        }
    };
  
  return (
        <Grid container>
    		<Grid item
                    width={{ xs: '100%', sm: '80%', md: '35%' }}
                    pl={{ xs: 0, md: '2.5%' }}
                    mt={{ xs: 60, sm: 0 }}>
                    <Grid container direction="column"
                        alignItems={{ xs: "flex-start", sm: "flex-end" }}>
                        <Grid item mt={{ xs: 0, md: '10vh' }} id="acum">
                            <Typography variant="h5" textAlign={{ xs: "left", sm: "right" }}
                                ref={addToRevealRefs}
                                className='gs_reveal_fromRight'>
                                NOW WE ARE IN
                            </Typography>
                        </Grid>

                        <Grid item>
                            <Typography variant="h6" textAlign={{ xs: "left", sm: "right" }}
                                ref={addToRevealRefs}
                                className='gs_reveal_fromRight'>
                                LOCATION
                            </Typography>
                        </Grid>

                        <Grid item mt="10vh" id="hi">
                            <Typography variant="h5" textAlign={{ xs: "left", sm: "right" }}
                                ref={addToRevealRefs}
                                className='gs_reveal_fromRight'>
                                SAY HI
                            </Typography>
                        </Grid>

                        <Grid item className='toughts'>
                            <Typography variant="h6" textAlign={{ xs: "left", sm: "right" }}
                                ref={addToRevealRefs}
                                className='gs_reveal_fromRight'>
                                TELL US YOUR THOUGHTS
                            </Typography>
                        </Grid>
                    </Grid>
                </Grid>
        </Grid>

}

HTML is longer and crowded, I left a part to get the idea of the structure and styling approach (MUI's sx - emotion).

 

And finally, this is the animateReveal function:

 

gs_reveal.js

import ScrollTrigger from 'gsap/ScrollTrigger';
import gsap from 'gsap';

export default function animateReveal(elements) {
    const triggers = []

    elements.forEach(function (elem) {
        hide(elem)

        let tr = ScrollTrigger.create({
            trigger: elem,
            id: elem.id,
            end: 'bottom top',
            markers: true,
            onEnter: function () { animateFrom(elem) },
            onEnterBack: function () { animateFrom(elem, -1) },
            onLeave: function () { hide(elem) }
        });
        triggers.push(tr)
    });

    return triggers;
}


function animateFrom(elem, direction) {
    direction = direction || 1;

    let x = 0,
        y = direction * 100;
    if (elem.classList.contains("gs_reveal_fromLeft")) {
        x = -100;
        y = 0;
    } else if (elem.classList.contains("gs_reveal_fromRight")) {
        x = 100;
        y = 0;
    }
    else if (elem.classList.contains("gs_reveal_fromBelow")) {
        y = -100
    }
    elem.style.transform = "translate(" + x + "px, " + y + "px)";
    elem.style.opacity = "0";

    gsap.fromTo(elem, { x: x, y: y, autoAlpha: 0 }, {
        duration: 1.25,
        x: 0,
        y: 0,
        autoAlpha: 1,
        ease: "expo",
        overwrite: "auto",
        delay: elem.classList.contains("gs_delay") ? 0.3 : 0,
    });
}

function hide(elem) {
    gsap.set(elem, { autoAlpha: 0 });
}

 

The ScrollTrigger markers are misplaced when page loads, and might move (get more misplaced) on hard reloading page, depending on the current scroll position in the moment of reloading, even though the scroll position is not preserved on reload (always is scrolled on top). - The markers are placed on the correct position on resizing, as expected.

 

I followed gsap official docs on react and react-advanced and tried:

  1. grabbing the html elements to animate on scroll inside animateReveal() by
    let elements = gsap.utils.toArray(".gs_reveal");
  2. Assigning to each element a useRef() and use the .current value for each in animateReveal()
  3. grabbing html elements using gsap's selector utility gsap.utils.selector
  4. changing to simpler animation on scroll, like just a fade
  5. refreshing ScrollTrigger in different moments
    useLayoutEffect(() => {
           ScrollTrigger.refresh(true) // or ScrollTrigger.refresh()
           ...
        }, []);

 

      6. Lifting ScrollTrigger logic to parent about.js page

      7. Assigning scrollTrigger to a timeline triggered by the to-be-reveal element

      8. Use useEffect() instead of useLayoutEffect() (recommended anyway for ScrollTrigger)

      7. Other who-knows-what unsuccessful twists.

 

I suspected a rehydration error, when the static generated code does not match the client side one. But the only JS that could cause a mismatch is the gsap related one, and it does not seem an SSR issue. I checked if the CSS and HTML elements are being properly SSR-ed, by preventing JS from running in the browser. All looking fine.

 

This is both a SSR issue (gatsby build) and a development issue (no SSR).

 

As i said on point 5, setting a ScrollTrigger.refresh() when component is mounted does not work, but delaying this with a 1-2 seconds in a setTimeout successfully solves the issue

    useLayoutEffect(() => {
        setTimeout(() => {
            ScrollTrigger.refresh(true)
        }, 2000);

    }, []);

This is hard to be accepted as a solution, since i cannot rely on a fixed value to 'guess' when DOM is properly rendered in the eyes of the ScrollTrigger, not to mention the glitches that might occur.

 

So, the question is 'WHY?', why animating with ScrollTrigger from within useLayoutEffect, which is not triggered on the server anyway and should mark the 'component is successfully mounted' moment, seems to not wait for the DOM being completely painted, even though nothing is generated dynamically!

 

There are quite of threads on this forum regarding gatsby, and none seemed to have a clear cause-outcome-solution. 

Is this battle lost, should i move on? Do you have any suggestions?

 

Thanks so much for your time reading this, it means so much to me!

 

Link to comment
Share on other sites

Hi @Vlad Tw

 

I followed your instructions and could not reproduce the problem during development or with a build. 

 

This is my repo.

https://github.com/OSUblake/gatsby-scrolltrigger

 

npm install
npm run develop

go to localhost:8000

 

Maybe someone else can clone that and see if they are seeing any issues because I'm at a lost. I wonder if it's just something with your environment. Have you tried it on another computer?

 

Link to comment
Share on other sites

Thank you @Cassie@OSUblake, @GreenSock, your help put an end to the mystery! 

 

Blake, the repo you provided truly has no issues and that threw me off, since our repos were almost identical, except few html elements.

 

As i said, i did not managed to get a consistent solution for ScrollTrigger. I figured out why after reading this:

https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-link/#handling-stale-client-side-pages.

 

The Gatsby Link component has intelligent preloading, and i guess that some stale client side page data is conflicting with the real elements, when the page that contains the ScrollTrigger is first accessed using the link, and not the actual navigation to localhost:8000/about.

 

index.js

export default function Index() {
  return (
    	...
        <Link to="/about" color="secondary">
          Go to the about page
        </Link>
       ...
  );
}

 

Now in order to reproduce the issue, just a small addition to your code. Please add the following somewhere in-between those grid items:

 

aboutBlock.js

          <Grid item
            id="breaking"
            ref={addToRevealRefs}
            sx={{
              "& img": {
                height: 'auto',
                width: '100%'
              }
            }}>
          	<img width="717" height="947" src="https://bukk.it/%3F%3F%3F%3Fcone%3F%3F%3F%3F%3F%3F%3F%3F.jpg" alt="aa" />
          </Grid>

Then:

  1. I would recommend deleting .cache and public folders to make sure there are no leftovers
  2. run: gatsby develop
  3.  open localhost:8000 
  4. CLICK on Go to about page link
  5. ScrollTrigger is broken 🙁
  6. (Can be tested in production build aswell, with gatsby build then gatsby serve-> localhost:9000)

I cannot say i know for sure why is this happening, but I identified two situations up until now:

  1. page contains images with css altered width/ height
  2. page contains textareas with number of rows > 1 ( I noticed that the SSR-ed page always contains 1 row textarea, the remaining rows being added dynamically by react, when js takes over)

 

Now again, this is happening IF GatsbyLink is used. If we replace that one with regular a, everything works fine. ✔️

 

index.js

export default function Index() {
  return (
    	...
        <a href="/about" color="secondary">
          Go to the about page
        </a>
       ...
  );
}

Replacing Gatsby Link with regular a tag would be a sad compromise when using GSAP with Gatsby, since the speed of loading a page would be drastically decreased. I strongly hope there could be some twist to this.

However, if this is a must, it should come to our attention, to not waste days mumbling in the dark.

 

What do you guys think?

 

*PS: thank you for reading this (again)! This forum does not cease to amaze me!

Link to comment
Share on other sites

Just now, Vlad Tw said:

Is this a valid solution on a heavy-image website? (impact on performance)

 

It should be fine, but a better option might be to make sure the images take up the required amount of space so it doesn't cause layout shifts when an image loads.

 

I thought browsers automatically do that when you have width and height attributes on an image, but it doesn't seem to be working in this case. 🤷‍♂️

 

Another way to make your images responsive while preventing layout shifts would be to use the padding-bottom trick.

https://stackoverflow.com/a/51496478/2760155

 

Link to comment
Share on other sites

4 minutes ago, OSUblake said:

I thought browsers automatically do that when you have width and height attributes on an image, but it doesn't seem to be working in this case. 🤷‍♂️

 

I thought that too! Not to mention that the rendered image gets correctly assigned in DOM, before JS and gsap takes over:

img[Attributes Style] {
    width: 717px;
    aspect-ratio: auto 717 / 947;
    height: 947px;
}

 

I'll investigate a bit more on this, but i think is all clear now from gsap's side. 

 

Thanks thanks a lot!

 

 

Link to comment
Share on other sites

  • 1 year later...

Hi guys,

 

It's my first time implementing Gsap with Gatsby and encountered myself facing exactly the same issue as described by @Vlad Tw.

 

In my case, it's for a typographic website with lot's of animations based on inlined svg, styled divs and text. So I don't have any image in order to use the <img src="..." onLoad={() => ScrollTrigger.refresh()} /> fix...

 

Replacing the Gatsby `Link` with regular `a` - as Vlad said - it would indeed solve my issue. But I must concur that it's not the ideal solution.

On 10/5/2021 at 6:52 PM, Vlad Tw said:

Replacing Gatsby Link with regular a tag would be a sad compromise when using GSAP with Gatsby, since the speed of loading a page would be drastically decreased. I strongly hope there could be some twist to this.

 

 

So I was wondering if any of you found another approach in solving this.

Thanks in advance!

 

Link to comment
Share on other sites

Hi @Nicolas Corso. Welcome to the community, and thanks for being a Club GreenSock member! 💚

 

This definitely sounds like more of a Gatsby question (I'm completely unfamiliar with Gatsby, sorry) - I assume you just need some way of tying into a lifecycle hook or something that basically says "the DOM has rendered and it's done shifting around and loading stuff" (so you can call ScrollTrigger.refresh()). Is there no such callback/hook in Gatsby? 

 

It'd be super amazingly helpful if you could provide a Stackblitz or CodeSandbox or something that replicates the issue so we can see it in context. But I think I heard that Gatsby doesn't work on either of those. Is that right? 

 

I wish I had more to offer, but perhaps @Rodrigo can chime in. 

  • Like 1
Link to comment
Share on other sites

Hi @GreenSock, thank you very much for your quick response and your kindly welcome!

 

Although it's my first time writing at the forum, I've been part of Club GreenSock for a while now and can't be happier with it :)

 

I completely understand this is more a Gatsby thing, not Gsap. But was worth trying!

 

I'm going to keep digging this issue, and in case I came up with a worthy solution, you can bet I'll post it here for this incredible community.

Best!

  • Like 1
Link to comment
Share on other sites

Hi @Nicolas Corso,

 

I'll dig into this tomorrow here on my local machine and see if I can come up with something that helps in this case. You mention that you don't have images in the page that is giving you troubles, so my only question at this point would be to ask if you've used useEffect or useLayoutEffect (for the case of this being SSR you can use the useIsomorphicLayout approach mentioned here).

 

Also have you tried with a setTimeout with 0 ms or very little time, like 10~50 milliseconds? Does your SVG has a lot of filters or anything that would delay the rendering process (I've seen that affect the loading process and create layout shifting in Next at least)?

 

Hopefully this helps and sorry for not being of more assitance.

Happy Tweening!

Link to comment
Share on other sites

Hi @Nicolas Corso,

 

I created a simple example using ScrollTrigger with Gatsby, that doesn't use images and even uses the gsap-trial package to morph an SVG and the MotionPath plugin with Gatsby's Link component for the routes, here:

https://github.com/rhernandog/gatsby-scrolltrigger

 

Please take a look at it and see if you can adapt that to your scenario. If you keep having any issues please provide a link to a minimal repo that shows the issue you're having, not your complete project.

 

Hopefully this helps.

Happy Tweening!

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...