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() {
|
export function Header() {
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [activeSection, setActiveSection] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
@@ -18,6 +19,42 @@ export function Header() {
|
|||||||
return () => window.removeEventListener("scroll", handleScroll);
|
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 (
|
return (
|
||||||
<header
|
<header
|
||||||
className={`fixed top-0 z-50 w-full transition-all duration-500 ${
|
className={`fixed top-0 z-50 w-full transition-all duration-500 ${
|
||||||
@@ -46,15 +83,22 @@ export function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden items-center gap-8 md:flex">
|
<nav className="hidden items-center gap-8 md:flex">
|
||||||
{NAV_LINKS.map((link) => (
|
{NAV_LINKS.map((link) => {
|
||||||
<a
|
const isActive = activeSection === link.href.replace("#", "");
|
||||||
key={link.href}
|
return (
|
||||||
href={link.href}
|
<a
|
||||||
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"
|
key={link.href}
|
||||||
>
|
href={link.href}
|
||||||
{link.label}
|
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 ${
|
||||||
</a>
|
isActive
|
||||||
))}
|
? "text-[#d4b87a] after:w-full"
|
||||||
|
: "text-neutral-400 after:w-0 hover:text-white hover:after:w-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 md:hidden">
|
<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 className="border-t border-white/[0.06] bg-black/40 px-6 py-4 backdrop-blur-xl sm:px-8">
|
||||||
{NAV_LINKS.map((link) => (
|
{NAV_LINKS.map((link) => {
|
||||||
<a
|
const isActive = activeSection === link.href.replace("#", "");
|
||||||
key={link.href}
|
return (
|
||||||
href={link.href}
|
<a
|
||||||
onClick={() => setMenuOpen(false)}
|
key={link.href}
|
||||||
className="block py-3 text-base text-neutral-400 transition-colors hover:text-white"
|
href={link.href}
|
||||||
>
|
onClick={() => setMenuOpen(false)}
|
||||||
{link.label}
|
className={`block py-3 text-base transition-colors ${
|
||||||
</a>
|
isActive
|
||||||
))}
|
? "text-[#d4b87a]"
|
||||||
|
: "text-neutral-400 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user