Jump to content
Search Community

Problem with GSAP timeline in NEXT JS

AKagzi test
Moderator Tag

Recommended Posts

Hello everyone,

I am working on a NextJS project with Tailwind CSS and new to GSAP. This is my code for the menu component <MobileMenu> in the mobile website which when clicked opens a full screen menu with close icon on the same place where menu open icon used to be (lets call it Original Position).

 

This is the animation i am trying to achieve .

But my component does not animates this way. I have been trying this to work since last 5 days and finally I am here to ask your help.

 

Original Position (Initial Position): The location where menu open and close icons are placed. Right side of the Header bar.

 

Objective:

  • Clicking the open menu icon should move the menu-trigger-bars to the top right direction out of the screen and close-trigger-bars should come to their place, i.e. to the original position and forming 'X' icon.
  • Clicking the close icon should do same animation but in reverse, i.e. close-trigger-bars going out of the screen and menu-trigger-bars coming back to the original position.

 

What my code does:

  • Triggering the menu open icon doesn't do any animation to the menu-trigger-bars but the close-trigger-bars do animate, not in the intended way. close-trigger-bars are moving to the bottom left and bottom right from the original position (where it should arrive). The motion of the close-trigger-bars plays twice, progressively continues going to their respective directions.
  • Triggering the menu close icon doesn't do any animation to the close-trigger-bars but the menu-trigger-bars do animate in their intended way, i.e. coming from top right side (outside of the screen) to the original position.

I have placed console.log to check the close and open animation. They are working fine and only once on every trigger.

 

What am I doing wrong? Please help me fix this issue.

 

@Rodrigo @GreenSock

 

'use client';

 

 

 
import { useState, useRef, useEffect } from 'react';
import { gsap } from 'gsap';
 
const MobileMenu = () => {
  const [isOpen, setIsOpen] = useState(false);
  const topBarRef = useRef(null);
  const closeTriggerRef = useRef(null);
 
  const tlOpen = useRef(gsap.timeline({ paused: true }));
  const tlClose = useRef(gsap.timeline({ paused: true }));
 
  useEffect(() => {
    // Animation for opening the menu
    tlOpen.current.add("preOpen")
      .to('.menu-trigger-bar.top', { x: 80, y: -80, delay: 0.1, ease: 'power4.in' }, "preOpen")
      .to('.menu-trigger-bar.middle', { x: 80, y: -80, ease: 'power4.in' }, "preOpen")
      .to('.menu-trigger-bar.bottom', { x: 80, y: -80, delay: 0.2, ease: 'power4.in' }, "preOpen")
      .add("open", "-=0.4")
      .add("preClose", "-=0.8")
      .to('.close-trigger-bar.left', { x: "-=100px", y: "+=100px", ease: 'power4.out' }, "preClose")
      .to('.close-trigger-bar.right', { x: "+=100px", y: "+=100px", delay: 0.2, ease: 'power4.out' }, "preClose");
 
    // Animation for closing the menu
    tlClose.current.add("close")
      .to('.close-trigger-bar.left', { x: "+=100px", y: "-=100px", ease: 'power4.in' }, "preClose")
      .to('.close-trigger-bar.right', { x: "-=100px", y: "-=100px", delay: 0.1, ease: 'power4.in' }, "preClose")
      .to('.menu-trigger-bar.top', { x: 0, y: 0, delay: 0.2, ease: 'power4.out' }, "close")
      .to('.menu-trigger-bar.middle', { x: 0, y: 0, ease: 'power4.out' }, "close")
      .to('.menu-trigger-bar.bottom', { x: 0, y: 0, delay: 0.3, ease: 'power4.out' }, "close");
  }, []);
 
  const toggleMenu = () => {
    if (isOpen) {
      tlClose.current.play(0); // Start the timeline from the beginning
      console.log('Playing close animation');
    } else {
      tlOpen.current.play(0); // Start the timeline from the beginning
      console.log('Playing open animation');
    }
    setIsOpen(!isOpen);
  };
 
  return (
    <>
      <button
        onClick={toggleMenu}
        className="text-gray-700 hover:text-gray-900 focus:outline-none z-50 relative w-8 h-8"
      >
        <span className="menu-trigger w-full h-full" style={{ display: isOpen ? 'none' : 'block' }}>
          <i ref={topBarRef} className="menu-trigger-bar top"></i>
          <i className="menu-trigger-bar middle"></i>
          <i className="menu-trigger-bar bottom"></i>
        </span>
        <span ref={closeTriggerRef} className="close-trigger w-full h-full" style={{ display: isOpen ? 'block' : 'none' }}>
          <i className="close-trigger-bar left"></i>
          <i className="close-trigger-bar right"></i>
        </span>
      </button>
      {isOpen && (
        <div className="fixed inset-0 bg-white z-40 flex flex-col items-center justify-center">
          <nav className="space-y-4">
            {/* Navigation links */}
            <a href="/" className="text-gray-700 hover:text-gray-900 text-2xl" onClick={toggleMenu}>Home</a>
            <a href="/about" className="text-gray-700 hover:text-gray-900 text-2xl" onClick={toggleMenu}>About</a>
            <a href="/services" className="text-gray-700 hover:text-gray-900 text-2xl" onClick={toggleMenu}>Services</a>
            <a href="/contact" className="text-gray-700 hover:text-gray-900 text-2xl" onClick={toggleMenu}>Contact</a>
          </nav>
        </div>
      )}
    </>
  );
};
 
export default MobileMenu;
 

See the Pen qBKyQYR by nothing4us (@nothing4us) on CodePen

Link to comment
Share on other sites

Hi there! I see you're using React -

Proper cleanup is very important with frameworks, but especially with React. React 18 runs in strict mode locally by default which causes your useEffect() and useLayoutEffect() to get called TWICE.

 

Since GSAP 3.12, we have the useGSAP() hook (the NPM package is here) that simplifies creating and cleaning up animations in React (including Next, Remix, etc). It's a drop-in replacement for useEffect()/useLayoutEffect(). All the GSAP-related objects (animations, ScrollTriggers, etc.) created while the function executes get collected and then reverted when the hook gets torn down.

 

Here is how it works:

const container = useRef(); // the root level element of your component (for scoping selector text which is optional)

useGSAP(() => {
  // gsap code here...
}, { dependencies: [endX], scope: container }); // config object offers maximum flexibility

Or if you prefer, you can use the same method signature as useEffect():

useGSAP(() => {
  // gsap code here...
}, [endX]); // simple dependency Array setup like useEffect()

This pattern follows React's best practices.

 

We strongly recommend reading the React guide we've put together at https://gsap.com/resources/React/

 

If you still need help, here's a React starter template that you can fork to create a minimal demo illustrating whatever issue you're running into. Post a link to your fork back here and we'd be happy to take a peek and answer any GSAP-related questions you have. Just use simple colored <div> elements in your demo; no need to recreate your whole project with client artwork, etc. The simpler the better. 

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