Jump to content
Search Community

Problem with GSAP Animation and Responsive Design in React

Georges-jean test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

First of all, a big thank you for taking the time to read this post. It's always comforting to see active, helpful communities, so first and foremost, thank you.

Next, I'll try to express my concern and hope that your experience will reveal something obvious that I've missed, as I'm not at all a GSAP expert (and not really a dev expert either, originally, I'm a Scrum Master...).
 

I'm working on a React site integrating GSAP animations and I'm facing a specific problem related to the management of responsive animations. The site works well, but I have issues when it comes to resizing the browser window.
 

I have a main component named StarrySky which displays various graphic elements (planets, etc.) and I want to apply different animations to certain elements: a vertical parallax effect via scrolling, simple rotation, and effects related to mouse movement. The positions of these elements are defined in vw to ensure a responsive design. I also have an external component, parallaxAnimation, to manage these animations, so I can reuse them wherever I want.

Problem: The animations work well at page load, but the positioning does not. Moreover, when I resize the window, the elements do not reposition correctly to the location normally generated by the CSS.
 

To solve the problem of repositioning elements during resizing, I created a method in StarrySky that transforms vw coordinates into px. I thought this would allow the animations to work correctly while maintaining the responsive quality of the vw. However, this did not solve the problem as I had hoped.

Here is an excerpt of my code to better illustrate my approach:
 

// Transformation from vw to pixels
const vwToPixels = (value) => {
  return value * (document.documentElement.clientWidth / 100);
};

// Updating the element's position from CSS
const updateElementPositionFromCss = (selector) => {
  const element = document.querySelector(selector);
  if (element) {
    const topPx = vwToPixels(parseFloat(window.getComputedStyle(element).top));
    const leftPx = vwToPixels(parseFloat(window.getComputedStyle(element).left));
    gsap.set(element, { x: leftPx, y: topPx });
  }
};

// Resizing function to update positions
const handleResize = () => {
  elementsData.forEach((element) => {
    updateElementPositionFromCss(`.${element.className}`);
  });
  ScrollTrigger.refresh();
};

window.addEventListener("resize", handleResize);

In addition to the animations, I have also set up specific management for window resizing to ensure that the GSAP animations adapt correctly to the new dimensions. The updateElementPositionFromCss function recalculates the positions of the elements based on their initial CSS styles, converting vw units into pixels. This is supposed to ensure that the animations remain aligned with the new layout of the page after resizing. However, I encounter problems where the elements do not reposition correctly after resizing, and the vertical parallax effect related to scrolling stops working.

I am looking for advice or suggestions to solve this problem. How can I maintain the reactivity of the elements while ensuring that the GSAP animations behave correctly during window resizing?
 

If more details or code snippets are needed to better understand the issue, I am more than willing to provide them.
 

Thank you again for your time and assistance!

Link to comment
Share on other sites

It's pretty tough to troubleshoot without a minimal demo - the issue could be caused by CSS, markup, a third party library, your browser, an external script that's totally unrelated to GSAP, etc. Would you please provide a very simple CodePen or Stackblitz that demonstrates the issue? 

 

Please don't include your whole project. Just some colored <div> elements and the GSAP code is best. See if you can recreate the issue with as few dependancies as possible. If not, incrementally add code bit by bit until it breaks. Usually people solve their own issues during this process! If not, then at least we have a reduced test case which greatly increases your chances of getting a relevant answer.

 

Here's a starter CodePen that loads all the plugins. Just click "fork" at the bottom right and make your minimal demo

See the Pen aYYOdN by GreenSock (@GreenSock) on CodePen

 

Using a framework/library like React, Vue, Next, etc.? 

CodePen isn't always ideal for these tools, so here are some Stackblitz starter templates that you can fork and import the gsap-trial NPM package for using any of the bonus plugins: 

 

Please share the StackBlitz link directly to the file in question (where you've put the GSAP code) so we don't need to hunt through all the files. 

 

Once we see an isolated demo, we'll do our best to jump in and help with your GSAP-specific questions. 

Link to comment
Share on other sites

Yeah, a minimal demo is gonna go a loooong way to getting you a solid answer here, but I'll mention a few things quickly: 

  1. You don't have to convert everything to px. You can use "vw" on transforms if you want to. 
    // this is fine to do: 
    gsap.set(element, { x: "50vw", y: "50vh" });

     

  2. You typically don't need to call ScrollTrigger.refresh() inside a "resize" event handler because ScrollTrigger automatically handles that for you in a debounced way. In other words, it's completely wasteful to do it in your code :) Be very careful about adding "resize" event handlers in general, as they can be pretty expensive because they fire a lot while the user is resizing the window.  And for the record, ScrollTrigger.refresh() is a pretty expensive function to run. 
  3. Have you seen gsap.matchMedia()? It might be a huge help in what you're trying to do. 
  4. Make sure you're either using gsap.context() or gsap.matchMedia() (which is just a specialized instance of gsap.context()) in React for easy cleanup. If you haven't read it yet, I HIGHLY recommend going to https://gsap.com/react

If you still need some help, please make sure you provide a minimal demo. Here's a Stackblitz you can fork: https://stackblitz.com/edit/react-cxv92j

Link to comment
Share on other sites

Hi there, thanks for the tips!
 

I've removed the vwToPx mechanism and the page resizing related methods from my code, but I'm still facing the positioning issue upon resizing the page. I've also implemented context.revert(); along with my existing animation cleanup routines. I believe they don't conflict with each other.

I've noticed that setting the scrollSpeed value to 0 (which is used to enhance the scrolling speed of the element) corrects the positioning, but then, of course, I lose the parallax scrolling effect. Additionally, I'm using lastScrollYPositions within my animations to store and track the last vertical position, ensuring that mouse movement and scrolling animations work well together.

Regarding gsap.matchMedia(), I haven't used it as it didn't seem necessary given that I'm using units like vw to keep the site responsive regardless of the page size.
 

I've created a minimal demo on StackBlitz to illustrate the issue. 
 

If necessary, I can also share the complete project via Git. I've just pushed the latest changes, though I assume it's preferable to stick with minimal demos.

Looking forward to any insights or suggestions you might have!

Link to comment
Share on other sites

Hi,

 

I don't have time right now to dive into all your code and make it work, but the issue could be in the fact that your GSAP instances should be created inside the scope of a GSAP Context instance. Right now you have something like this:

const myMethod = () => {
  const t = gsap.to(element, { x: 200 });

  const cleanupMethod = () => {
    t.kill();
  };

  return cleanupMethod;
};

useLayoutEffect(() => {
  let cleanup;
  const ctx = gsap.context(() => {
    cleanup = myMethod();
  });
  return () => {
    ctx.revert();
    cleanup();
  }; 
}, []);

While there are no logical errors in that code is not doing what you expect. Sure enough myMethod is called inside the execution context of the gsap.context() instance, but those tweens are not added to that GSAP Context, to the cleanup method, while it kills the animation, it doesn't revert everything to what it was before myMethod was called.

 

It should be a bit like this:

const myMethod = () => {
  const t = gsap.to(element, { x: 200 });

  return t;
};

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    myMethod();
  });
  return () => ctx.revert();
}, []);

Like that the specific GSAP Tween is added to the GSAP Context instance.

 

Another option would be this:

const myMethod = (ctx) => {
  ctx.add(() => {
    const t = gsap.to(element, { x: 200 });
  });
};

useLayoutEffect(() => {
  const ctx = gsap.context((self) => {
    myMethod(self);
  });
  return () => ctx.revert();
}, []);

Hopefully this helps.

Happy Tweening!

Link to comment
Share on other sites

I haven't responded to this thread in a few days as I've been spending a lot of time trying to incorporate the GSAP context into my animations as suggested in the last message. The issue I encountered was my desire to maintain my architecture, wherein I wanted to place my animations in a separate file.

 

Unfortunately, I faced numerous blockers due to the context not being available when passed to the separate file. In the end, I abandoned the separate animation architecture and placed them directly in the main file. However, this did not solve my positioning issues related to the combined effects of vertical parallax through scrolling and resizing. Thus, I had to rethink my approach.

 

To address the repositioning issue, I employed a simple method - new to me - which is the timeline. I had to revise some of my code, but now I no longer face repositioning issues due to resizing. However, I have encountered another problem for which I am seeking help.

 

Currently, my file contains three combined animations: rotation, a parallax effect through mouse movement, and a parallax effect through scrolling.

The problem is a conflict between the modification of 'y' through scrolling and the mousemove, which blocks the 'y' position update during scrolling. This update only occurs when I activate mousemove. I've tried saving the 'y' position of the element, but the results are not convincing. This is currently the closest result to what I aim for, but perhaps there is an issue with my approach. I have experimented with using timelines, but maybe it's not necessary after all. Here is the demo.

Link to comment
Share on other sites

  • Solution

It looks to me like an engineering problem - you're trying to have two different things affect the same property of the same element simultaneously, in different directions/amounts. If I understand your goal correctly, the simplest solution would be to just create a wrapper around your elements so that your scroll-based parallax affect the wrapper, and the mouse movement affects the element itself. 

 

Here's a fork that also uses our brand new (unannounced) @gsap/react useGSAP() hook to make things a little easier: 

https://stackblitz.com/edit/react-an2k6t?file=src%2FApp.js 

 

I also optimized the mousemove stuff by using gsap.quickTo()

 

I hope that helps. 

  • Like 3
Link to comment
Share on other sites

Ok, it's even more frustrating when the code and animation I want works in the mini-demo you sent me : 
https://stackblitz.com/edit/react-an2k6t?file=src%2FApp.js
But it doesn't work in my project... I've used the code to the letter and I get this behaviour: when I start the scroll, the position of my object is teleported to the bottom of my component.
I tried using a simple css object or playing with the scrollspeed, but nothing.
Here's the code I used, practically the same as in the minimal demo, I just added an onUpdate to follow the 'y' but it didn't tell me much: 

    // Configuration de la parallaxe verticale avec ScrollTrigger et une timeline
    const parallaxTimeline = gsap.timeline({
      scrollTrigger: {
        trigger: ".starrySkySection",
        start: "top top",
        end: "=+1000",
        scrub: true,
        onUpdate: (self) => {
          elementsData.forEach((element) => {
            if (element.scrollSpeed) {
              const yPos =
                self.progress * (window.innerHeight * element.scrollSpeed);
              console.log(`Y position for ${element.className}: ${yPos}`);
              parallaxYPositions.current[element.className] = yPos;
            }
          });
        },
      },
    });

    elementsData.forEach((element) => {
      if (element.scrollSpeed) {
        parallaxYPositions.current[element.className] = 0;
        parallaxTimeline.to(
          document.querySelector(`.${element.className}`).parentNode,
          {
            y: `+=${window.innerHeight * element.scrollSpeed}`,
            ease: "none",
          },
          0
        );
      }
    });

 

Link to comment
Share on other sites

Hm, we can't really troubleshoot by just looking at a small excerpt of code like that - we really need to see the problem in context (hence the requests for a minimal demo that clearly illustrates the issue). If the code works in the demo but not in your project, it means there must be something different in your project that's interfering, so the task becomes taking the minimal demo and slowly making it more and more like your real project until it breaks. Then you know exactly what change you made that broke things and that'll lead to a solution. 

 

Are you using the latest version of GSAP in your project? Do you maybe have some CSS issue that's interfering? Are you doing proper cleanup in React? Are you using the useGSAP() hook? 

 

Once we see a minimal demo that clearly illustrates the issue, I'm sure we'll be able to offer some advice. 

Link to comment
Share on other sites

I'm going to continue investigating my issue as advised in the last message, but I'd like to report a strange behavior I've observed.
'm using roughly the same code as in this minimal demo, and I don't have any external code that seems to interact with these elements. Apart from this component, I've only developed my navbar.
espite this, I can't replicate the animations from the minimal demo except when I save my code in my IDE (thus recompiling) but don't refresh the page. As soon as I refresh the page, my animation stops working...

Do you have any suggestions for troubleshooting based on this observation? I'm using the same code as the minimal-demo, so yes, I am using the useGSAP() hook. Regarding versions, here are my dependencies, which I think are up to date:

  "dependencies": {
    "@gsap/react": "^1.0.0",
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "gsap": "^3.12.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
Link to comment
Share on other sites

Hey,

I was able to reproduce one of the bugs that has been bothering me in the minimal-demo...
 

While trying to explain my issue here, it served me as rubberducking, and I found the issue. It was related to the size of my background, which is very long, causing the animation to glitch. When I mocked this background in the minimal-demo, I realized that its CSS was set to 'display: block,' which was causing the ScrollTrigger to malfunction. It makes sense to me now; it took some time, but I learned a lot along the way!

Thank you for your assistance; I'll mark this as resolved.
Best regards!

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