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!