feat: highlight active section in header nav on scroll
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { HeroLogo } from "@/components/ui/HeroLogo";
|
||||
export function Header() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
function handleScroll() {
|
||||
@@ -18,6 +19,42 @@ export function Header() {
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const sectionIds = NAV_LINKS.map((l) => l.href.replace("#", ""));
|
||||
const observers: IntersectionObserver[] = [];
|
||||
|
||||
// Observe hero — when visible, clear active section
|
||||
const hero = document.querySelector("section:first-of-type");
|
||||
if (hero) {
|
||||
const heroObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) setActiveSection("");
|
||||
},
|
||||
{ rootMargin: "-20% 0px -70% 0px" },
|
||||
);
|
||||
heroObserver.observe(hero);
|
||||
observers.push(heroObserver);
|
||||
}
|
||||
|
||||
sectionIds.forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveSection(id);
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-40% 0px -55% 0px" },
|
||||
);
|
||||
observer.observe(el);
|
||||
observers.push(observer);
|
||||
});
|
||||
|
||||
return () => observers.forEach((o) => o.disconnect());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 z-50 w-full transition-all duration-500 ${
|
||||
@@ -46,15 +83,22 @@ export function Header() {
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-8 md:flex">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="relative py-1 text-sm font-medium text-neutral-400 transition-all duration-300 after:absolute after:bottom-0 after:left-0 after:h-[2px] after:w-0 after:bg-[#c9a96e] after:transition-all after:duration-300 hover:text-white hover:after:w-full"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = activeSection === link.href.replace("#", "");
|
||||
return (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`relative py-1 text-sm font-medium transition-all duration-300 after:absolute after:bottom-0 after:left-0 after:h-[2px] after:bg-[#c9a96e] after:transition-all after:duration-300 ${
|
||||
isActive
|
||||
? "text-[#d4b87a] after:w-full"
|
||||
: "text-neutral-400 after:w-0 hover:text-white hover:after:w-full"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
@@ -75,16 +119,23 @@ export function Header() {
|
||||
}`}
|
||||
>
|
||||
<nav className="border-t border-white/[0.06] bg-black/40 px-6 py-4 backdrop-blur-xl sm:px-8">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="block py-3 text-base text-neutral-400 transition-colors hover:text-white"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = activeSection === link.href.replace("#", "");
|
||||
return (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className={`block py-3 text-base transition-colors ${
|
||||
isActive
|
||||
? "text-[#d4b87a]"
|
||||
: "text-neutral-400 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user