Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
Vlad Tw

ScrollTrigger issue with Gatsby

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

Oh man this sounds super frustrating Vlad.

It sounds like you've done all the right things, I'm afraid I don't have any suggestions.

%3F%3F%3F%3Fcone%3F%3F%3F%3F%3F%3F%3F%3F

I'll tag in @OSUblake - he's the React brains around here.

Hopefully he'll be able to help you out.

  • Like 1
Link to comment
Share on other sites

I'll do a build following the instructions you provided and will report back later. 😉

  • Thanks 1
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

I don't see any issues but maybe I'm missing something. 🤷‍♂️

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

  • Solution

Hi @Vlad Tw

 

I'm not sure about the textarea, but for the image, when I add an onLoad listener like this, it seems to work as expected. Does that fix it for you?

<img src="..." onLoad={() => ScrollTrigger.refresh()} />

 

Link to comment
Share on other sites

Yup, that works @OSUblake! Can't even be happy at this point, but truly grateful. 💚

 

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

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

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