Indicator Navigation Animation: Using GSAP and tailwindcss

·

Day 1 create navigation indication animation, inspired by this page. I love how moving around the indicator of nav bar. I re-create this using vite, tailwind and gsap.

First of all, I create html code

HTML
<header class="bg-white rounded-lg shadow-md flex items-center p-6">
        <img src="/vite.svg" alt="Vite Logo" />
        <nav
          class="flex justify-center ml-8 p-1.5 bg-(--nav-background) rounded-full inset-shadow-sm"
        >
          <ul
            class="relative z-0 flex text-(--nav-foreground) font-semibold *:py-3 *:px-5 *:rounded-full *:hover:text-(--nav-foreground-active) *:text-(--nav-foreground) *:line-clamp-1"
          >
            <div
              class="absolute bg-white rounded-full p-3 shadow-sm"
              id="nav-indicator"
              style="
                top: 0;
                left: 1.2rem;
                z-index: -1;
                transition: left 0.3s ease-in-out;
              "
            >
               
            </div>

            <li class=""><a href="/">Home</a></li>
            <li class=""><a href="/about">About</a></li>
            <li class=""><a href="/ecosytem">Ecosystem</a></li>
            <li class=""><a href="/services">Other Think</a></li>
            <li class=""><a href="/contact">Looooooonng Menu</a></li>
          </ul>
        </nav>
      </header>

And then, these basic structure is enough with nav-indicator as button–moving–thing. and then I add javascript script to make it move.

TypeScript
import gsap from 'gsap';
console.log('Vite configuration loaded');

function navIndicatorMovement({
  navIndicatorElement,
  ulElement,
}: {
  navIndicatorElement: HTMLElement;
  ulElement: HTMLUListElement;
}) {
  const liElements = ulElement.querySelectorAll('li');


  function setIndicatorPosition(liElement: HTMLLIElement) {
    const liRect = liElement.getBoundingClientRect();
    const ulRect = ulElement.getBoundingClientRect();
    console.log(`liRect.left: ${liRect.left}, ulRect.left: ${ulRect.left}`);
    const offsetX = (liRect.left - ulRect.left)

    gsap.to(navIndicatorElement, {
      left: offsetX,
      width: liRect.width,
      duration: 0.1,
      ease: 'power2',
    })
  }

  // init
  if (liElements.length > 0) {
    const firstLiElement = liElements[0] as HTMLLIElement;
    setIndicatorPosition(firstLiElement);
  }

  liElements.forEach((liElement) => {
    let debounceTimeout: NodeJS.Timeout | null = null;
    liElement.addEventListener('mouseover', () => {
      if (debounceTimeout) {
        clearTimeout(debounceTimeout);
      }
      debounceTimeout = setTimeout(() => {
        setIndicatorPosition(liElement as HTMLLIElement);
      }, 100);
    })
    // on mobile, on tap
    liElement.addEventListener('click', (e) => {
      e.preventDefault();
      if (debounceTimeout) {
        clearTimeout(debounceTimeout);
      }
      setIndicatorPosition(liElement as HTMLLIElement);
    })
  })
}

window.addEventListener('DOMContentLoaded', () => {
  const navIndicatorElement = document.querySelector('#nav-indicator') as HTMLElement;

  const ulElement = document.querySelector('ul') as HTMLUListElement;

  if (navIndicatorElement && ulElement) {
    navIndicatorMovement({ navIndicatorElement, ulElement });
  } else {
    console.error('Navigation indicator or UL element not found');
  }
});

It also support other situation:

  • Mobile tap should move the indicator
  • and add debounce to delay it.
  • Initial menu should selected

So, that’s it. It’s simple but powerful to create micro animation. Thank you for your reading!