Jump to content
Search Community

Having body text in column display on hover (next.js)

jnemec test
Moderator Tag

Recommended Posts

So I'm trying to be able to hover over a column and have its "body" text slide upward. I have it set up how I want it except the body text only slides up on the last column regardless of where I place my cursor. I've been working at this all day and can't seem to get it working. Any help would be appreciated 

 

https://codesandbox.io/p/sandbox/gsap-columns-rgi7s2

  • Like 1
Link to comment
Share on other sites

1 hour ago, Cassie said:

I can't find the right file in that sandbox at a glance, That's quite a large project structure.

But this sounds like a scoping issue, you're likely always targeting the last container
 

 

THANK YOU! So the file is in the "components" folder and then the "Hero.tsx" file. What you sent is exactly what I am wanting. I'll take a look at it and hopefully I can get it working. Struggling a little with integrating GSAP in next.js a little bit 

  • Like 1
Link to comment
Share on other sites

Hi,

 

As Cassie mentions this is mostly a scope issue. Right now you have this:

{columns?.map((column, i) => (
  <div
    key={i}
    className={`column ${styles.center}`}
    onMouseEnter={() => handleMouseEnter(i)}
    onMouseLeave={() => handleMouseLeave(i)}
    style={{ backgroundColor: column.backgroundColor }}
  >
    <div className={styles.content}>
      <h2 className={styles.label}>{column?.label}</h2>
      <p className={`body-text ${styles.body}`} ref={bodyRef}>// <- This Ref
        {column?.body}
      </p>
    </div>
  </div>
))}

That particular ref is just a single element, so everytime the loop runs it gets replaced with the current iteration. Logically this loop will end with the last element, that's why you're seeing this result which is totally expected. Unfortunately unlinke Vue, React doesn't create an array of refs when looping through a collection, so you have to take care of that manually.

 

There are two ways to solve this. One is to use a callback ref:

https://reactjs.org/docs/refs-and-the-dom.html#callback-refs

 

The other is to use GSAP Context's scoping superpowers and enjoy life :D:

const Hero = () => {
  const ctx = useRef(null);
  const columnRef = useRef(null);
  
  const handleMouseEnter = (i:Number) => {
    ctx.current.mouseEnterHandler(i);
  };
  const handleMouseLeave = (i:Number) => {
    ctx.current.mouseLeaveHandler(i);
  };

  useLayoutEffect(() => {
    ctx.current = gsap.context((self) => {
      gsap.from(columnRef.current.children, {
        opacity: 0,
        x: 200,
        stagger: 0.3,
      });

      const columns = gsap.utils.toArray(".column");
      const texts = columns.map((column: HTMLDivElement) =>
        column.querySelector(".body-text")
      );
      gsap.set(texts, {
        opacity: 0,
        y: 200,
      });
      self.add("mouseEnterHandler", (i) => {
        gsap.to(texts[i], {
          opacity: 1,
          y: 0,
          duration: 1,
        });
      });
      self.add("mouseLeaveHandler", (i) => {
        gsap.to(texts[i], {
          opacity: 0,
          y: 200,
          duration: 1,
        });
      });
    }, columnRef);
    return () => ctx.current.revert();
  }, []);

  return (
    <section className={styles.section}>
      <div className={styles.columns} ref={columnRef}>
        {columns?.map((column, i) => (
          <div
            key={i}
            className={`column ${styles.center}`}
            onMouseEnter={() => handleMouseEnter(i)}
            onMouseLeave={() => handleMouseLeave(i)}
            style={{ backgroundColor: column.backgroundColor }}
          >
            <div className={styles.content}>
              <h2 className={styles.label}>{column?.label}</h2>
              <p className={`body-text ${styles.body}`}>
                {column?.body}
              </p>
            </div>
          </div>
        ))}
      </div>
    </section>
  );
};

I forked your example so you can see it in action:

https://codesandbox.io/p/sandbox/gsap-columns-forked-tl8bec?file=%2Fcomponents%2FHero.tsx

 

Finally we'd like to know specifically what are the biggest pain points when integrating GSAP in your NextJS projects. Anything in the Docs and/or official React guides that seems confusing or hard to follow/understand. We thrive in improving the resources we have available for our users in order to ease GSAP integration in their projects.

 

Happy Tweening!

  • Like 3
Link to comment
Share on other sites

56 minutes ago, Rodrigo said:

Hi,

 

As Cassie mentions this is mostly a scope issue. Right now you have this:

{columns?.map((column, i) => (
  <div
    key={i}
    className={`column ${styles.center}`}
    onMouseEnter={() => handleMouseEnter(i)}
    onMouseLeave={() => handleMouseLeave(i)}
    style={{ backgroundColor: column.backgroundColor }}
  >
    <div className={styles.content}>
      <h2 className={styles.label}>{column?.label}</h2>
      <p className={`body-text ${styles.body}`} ref={bodyRef}>// <- This Ref
        {column?.body}
      </p>
    </div>
  </div>
))}

That particular ref is just a single element, so everytime the loop runs it gets replaced with the current iteration. Logically this loop will end with the last element, that's why you're seeing this result which is totally expected. Unfortunately unlinke Vue, React doesn't create an array of refs when looping through a collection, so you have to take care of that manually.

 

There are two ways to solve this. One is to use a callback ref:

https://reactjs.org/docs/refs-and-the-dom.html#callback-refs

 

The other is to use GSAP Context's scoping superpowers and enjoy life :D:

const Hero = () => {
  const ctx = useRef(null);
  const columnRef = useRef(null);
  
  const handleMouseEnter = (i:Number) => {
    ctx.current.mouseEnterHandler(i);
  };
  const handleMouseLeave = (i:Number) => {
    ctx.current.mouseLeaveHandler(i);
  };

  useLayoutEffect(() => {
    ctx.current = gsap.context((self) => {
      gsap.from(columnRef.current.children, {
        opacity: 0,
        x: 200,
        stagger: 0.3,
      });

      const columns = gsap.utils.toArray(".column");
      const texts = columns.map((column: HTMLDivElement) =>
        column.querySelector(".body-text")
      );
      gsap.set(texts, {
        opacity: 0,
        y: 200,
      });
      self.add("mouseEnterHandler", (i) => {
        gsap.to(texts[i], {
          opacity: 1,
          y: 0,
          duration: 1,
        });
      });
      self.add("mouseLeaveHandler", (i) => {
        gsap.to(texts[i], {
          opacity: 0,
          y: 200,
          duration: 1,
        });
      });
    }, columnRef);
    return () => ctx.current.revert();
  }, []);

  return (
    <section className={styles.section}>
      <div className={styles.columns} ref={columnRef}>
        {columns?.map((column, i) => (
          <div
            key={i}
            className={`column ${styles.center}`}
            onMouseEnter={() => handleMouseEnter(i)}
            onMouseLeave={() => handleMouseLeave(i)}
            style={{ backgroundColor: column.backgroundColor }}
          >
            <div className={styles.content}>
              <h2 className={styles.label}>{column?.label}</h2>
              <p className={`body-text ${styles.body}`}>
                {column?.body}
              </p>
            </div>
          </div>
        ))}
      </div>
    </section>
  );
};

I forked your example so you can see it in action:

https://codesandbox.io/p/sandbox/gsap-columns-forked-tl8bec?file=%2Fcomponents%2FHero.tsx

 

Finally we'd like to know specifically what are the biggest pain points when integrating GSAP in your NextJS projects. Anything in the Docs and/or official React guides that seems confusing or hard to follow/understand. We thrive in improving the resources we have available for our users in order to ease GSAP integration in their projects.

 

Happy Tweening!

This worked great! Thank you so much. Personally they only thing I struggled with was trying to implement GSAP while mapping over an array of objects. I couldn't really find much information about that. I also think it would be awesome if there were more videos of using GSAP with React etc. 

  • Like 1
Link to comment
Share on other sites

On 3/14/2023 at 9:17 AM, Rodrigo said:

Hi,

 

As Cassie mentions this is mostly a scope issue. Right now you have this:

{columns?.map((column, i) => (
  <div
    key={i}
    className={`column ${styles.center}`}
    onMouseEnter={() => handleMouseEnter(i)}
    onMouseLeave={() => handleMouseLeave(i)}
    style={{ backgroundColor: column.backgroundColor }}
  >
    <div className={styles.content}>
      <h2 className={styles.label}>{column?.label}</h2>
      <p className={`body-text ${styles.body}`} ref={bodyRef}>// <- This Ref
        {column?.body}
      </p>
    </div>
  </div>
))}

That particular ref is just a single element, so everytime the loop runs it gets replaced with the current iteration. Logically this loop will end with the last element, that's why you're seeing this result which is totally expected. Unfortunately unlinke Vue, React doesn't create an array of refs when looping through a collection, so you have to take care of that manually.

 

There are two ways to solve this. One is to use a callback ref:

https://reactjs.org/docs/refs-and-the-dom.html#callback-refs

 

The other is to use GSAP Context's scoping superpowers and enjoy life :D:

const Hero = () => {
  const ctx = useRef(null);
  const columnRef = useRef(null);
  
  const handleMouseEnter = (i:Number) => {
    ctx.current.mouseEnterHandler(i);
  };
  const handleMouseLeave = (i:Number) => {
    ctx.current.mouseLeaveHandler(i);
  };

  useLayoutEffect(() => {
    ctx.current = gsap.context((self) => {
      gsap.from(columnRef.current.children, {
        opacity: 0,
        x: 200,
        stagger: 0.3,
      });

      const columns = gsap.utils.toArray(".column");
      const texts = columns.map((column: HTMLDivElement) =>
        column.querySelector(".body-text")
      );
      gsap.set(texts, {
        opacity: 0,
        y: 200,
      });
      self.add("mouseEnterHandler", (i) => {
        gsap.to(texts[i], {
          opacity: 1,
          y: 0,
          duration: 1,
        });
      });
      self.add("mouseLeaveHandler", (i) => {
        gsap.to(texts[i], {
          opacity: 0,
          y: 200,
          duration: 1,
        });
      });
    }, columnRef);
    return () => ctx.current.revert();
  }, []);

  return (
    <section className={styles.section}>
      <div className={styles.columns} ref={columnRef}>
        {columns?.map((column, i) => (
          <div
            key={i}
            className={`column ${styles.center}`}
            onMouseEnter={() => handleMouseEnter(i)}
            onMouseLeave={() => handleMouseLeave(i)}
            style={{ backgroundColor: column.backgroundColor }}
          >
            <div className={styles.content}>
              <h2 className={styles.label}>{column?.label}</h2>
              <p className={`body-text ${styles.body}`}>
                {column?.body}
              </p>
            </div>
          </div>
        ))}
      </div>
    </section>
  );
};

I forked your example so you can see it in action:

https://codesandbox.io/p/sandbox/gsap-columns-forked-tl8bec?file=%2Fcomponents%2FHero.tsx

 

Finally we'd like to know specifically what are the biggest pain points when integrating GSAP in your NextJS projects. Anything in the Docs and/or official React guides that seems confusing or hard to follow/understand. We thrive in improving the resources we have available for our users in order to ease GSAP integration in their projects.

 

Happy Tweening!

I'm still working on this, but you have been a lot of help.I was wondering why you created a ctx ref? I haven't seen that done before and just want to be able to understand what's going on. Thanks!

Link to comment
Share on other sites

Hi,

 

Because I have to access the methods added to the GSAP Context instance in the effect hook. Keep in mind that the handlers and the effect hook have different scopes, since each one is a different function, so they don't have a clue of what's going on inside each other.

 

In this example:

const methodA = () => {
  const a = foo;
};

const methodB = () => {
  const b = bar;
};

There is no way I can access a in methodB, right? Well is the same here:

const myHandler = () => {
  ctx.myMethod(); // ERROR!
};

useLayoutEffect(() => {
  const ctx = gsap.context((self) => {
    self.add("myMethod", () => {
      gsap.to(".some-element", { x: 200 });
    });
  });
}, []);

In this case ctx is defined inside the effect hook, so it only exists on that particular scope or execution context. The myHandler function has no idea about ctx, so we need to put it in an execution context that both share and that is the root of the React component. Since we don't need ctx to be created on each re-render of the component, we put it on a ref so it's kept through re-renders.

 

https://www.freecodecamp.org/news/how-javascript-works-behind-the-scene-javascript-execution-context/

 

Hopefully this clear things up.

Happy Tweening!

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