Mon Nov 18 2024
How to Highlight a Menu on Scroll of the Page Using Next.js and TailwindCSS
If you want to create a dynamic menu that highlights the current section as you scroll through a webpage on a Next.js application, you can do it very easily using React UseEffect hook. It is a common feature in modern web development. This functionality provides a better user experience by indicating which section of the page the user is currently viewing. In this article, I’ll show you how to achieve this functionality using Next.js and TailwindCSS.
Step 1: Create Menu/Section Array
First I created an array which has multiple menus/sections.
const menus = ["home", "packages", "about", "gallery", "contact"];
Step 2: Create the Header Component
Next, I created a simple header component of the page where I iterated the menus and decorated it with TailwindCSS. This header should stick at the top, so that it can be visible for all the sections.
<header className="fixed left-0 right-0 z-50 bg-black bg-opacity-50 py-4">
/* For Mobile Hamburger Menu */
<div className="px-4 flex justify-end sm:hidden">
<button
className="cursor-pointer"
onClick={() => setToggleMenu(!toggleMenu)}
>
<svg
id="Layer_1"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 246.42 246.04"
className="fill-white w-8 h-8"
>
<rect x="0.79" y="30.22" width="245.63" height="23.36" rx="11.68" />
<rect
x="0.39"
y="111.32"
width="245.63"
height="23.36"
rx="11.68"
/>
<rect y="192.42" width="245.63" height="23.36" rx="11.68" />
</svg>
</button>
</div>
<nav
className={`${
toggleMenu ? "flex" : "hidden sm:flex"
} justify-center items-center gap-3 sm:gap-5 lg:gap-10 sm:flex-row flex-col mt-2 sm:mt-0`}
>
{menus.map((menu: string, i: number) => {
return (
<a
key={i}
href={`#${menu}`}
className={`w-full sm:w-auto uppercase font-semibold text-base text-white text-center sm:px-3 lg:px-5 py-2 sm:py-1 rounded-2xl transition-all ease-linear hover:bg-green hover:shadow-md`}
>
{menu}
</a>
);
})}
</nav>
</header>
Step 3: Smooth Scrolling Behavior
To enable smooth scrolling when clicking on a menu link, we need to add CSS for smooth scrolling on html tag on our globals.css file.
html {
scroll-behavior: smooth;
}
Step 4: Highlights the Active Menu Item
Using React UseEffect hook, recognize all the sections by its id. And using IntersectionObserver in the menubar component to observe when each section enters or leaves the viewport. This is an efficient way to detect which section is currently in view. I also use a React State variable where I track the active section. When the observer detects that a section is in view, it updates this state.
const [activeSection, setActiveSection] = useState(null);
useEffect(() => {
const options = {
root: null, // Use the viewport as the root
rootMargin: "0px",
threshold: 0.4, // Trigger when 40% of the section is visible
};
const observer = new IntersectionObserver((entries: any) => {
entries.forEach((entry: any) => {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
}
});
}, options);
menus.forEach((menu: string) => {
const element = document.getElementById(menu);
if (element) observer.observe(element);
});
// Cleanup observer on unmount
return () => observer.disconnect();
}, [menus]);
Step 5: Adding Active Menu Styling
Based on this active section I’m applying conditional styling to the menu item.
className={`.......... ${activeSection == menu ? "bg-green shadow-md" : ""}`}
You can test the code and extend it further with animations, icons, or responsive features. You can also download the full template from WebGraphiz.com at free of cost. Happy coding!
File Name: header.tsx
"use client";
import { useEffect, useState } from "react";
const menus = ["home", "packages", "about", "gallery", "contact"];
const Header = () => {
const [activeSection, setActiveSection] = useState(null);
const [toggleMenu, setToggleMenu] = useState(false);
useEffect(() => {
const options = {
root: null,
rootMargin: "0px",
threshold: 0.4,
};
const observer = new IntersectionObserver((entries: any) => {
entries.forEach((entry: any) => {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
}
});
}, options);
menus.forEach((menu: string) => {
const element = document.getElementById(menu);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, [menus]);
return (
<header className="fixed left-0 right-0 z-50 bg-black bg-opacity-50 py-4">
<div className="px-4 flex justify-end sm:hidden">
<button
className="cursor-pointer"
onClick={() => setToggleMenu(!toggleMenu)}
>
<svg
id="Layer_1"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 246.42 246.04"
className="fill-white w-8 h-8"
>
<rect x="0.79" y="30.22" width="245.63" height="23.36" rx="11.68" />
<rect
x="0.39"
y="111.32"
width="245.63"
height="23.36"
rx="11.68"
/>
<rect y="192.42" width="245.63" height="23.36" rx="11.68" />
</svg>
</button>
</div>
<nav
className={`${
toggleMenu ? "flex" : "hidden sm:flex"
} justify-center items-center gap-3 sm:gap-5 lg:gap-10 sm:flex-row flex-col mt-2 sm:mt-0`}
>
{menus.map((menu: string, i: number) => {
return (
<a
key={i}
href={`#${menu}`}
className={`w-full sm:w-auto uppercase font-semibold text-base text-white text-center sm:px-3 lg:px-5 py-2 sm:py-1 rounded-2xl transition-all ease-linear hover:bg-green hover:shadow-md ${
activeSection == menu ? "bg-green shadow-md" : ""
}`}
>
{menu}
</a>
);
})}
</nav>
</header>
);
};
export default Header;