Navbar that sits transparent over a photo hero and turns solid on scroll (from Marbrook)
"use client";
import { Menu, Phone, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetClose,
SheetPopup,
SheetTrigger,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
import type { NavbarContent } from "../_types/navbar";
import { defaultNavbar5Content } from "./data";
/** `/properties/blackwood-house` keeps the Properties link lit. */
function isActive(activeHref: string | undefined, href: string): boolean {
if (!activeHref) return false;
return activeHref === href || activeHref.startsWith(`${href}/`);
}
function telHref(phone: string): string {
return `tel:${phone.replace(/[^\d+]/g, "")}`;
}
export default function Navbar5({
activeHref,
content = defaultNavbar5Content,
fixed = false,
overlay = false,
}: {
/** The current path, used to underline the matching link. */
activeHref?: string;
content?: NavbarContent;
/** Pin the bar to the top of the viewport. */
fixed?: boolean;
/** Transparent with white text until the page scrolls (sit it over a photo hero). */
overlay?: boolean;
}) {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
if (!overlay) return;
const handleScroll = () => setScrolled(window.scrollY > 24);
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [overlay]);
const solid = !overlay || scrolled;
const { brand, cta, links, phone } = content;
return (
<header
className={cn(
"inset-x-0 top-0 z-50 transition-colors duration-300",
fixed ? "fixed" : overlay ? "absolute" : "relative",
solid
? "border-border/80 border-b bg-background/95 backdrop-blur-md"
: "bg-transparent",
)}
>
<div className="mx-auto flex max-w-7xl items-center justify-between gap-6 px-5 py-4 sm:px-8 lg:px-12">
<Link
className={cn(
"flex items-baseline gap-2 transition-colors",
solid ? "text-primary" : "text-white",
)}
href={brand.href}
>
<span className="font-display text-2xl leading-none tracking-tight">
{brand.name}
</span>
{brand.tagline && (
<span
className={cn(
"hidden font-mono text-[0.625rem] uppercase tracking-[0.2em] sm:inline",
solid ? "text-muted-foreground/70" : "text-white/60",
)}
>
{brand.tagline}
</span>
)}
</Link>
<nav className="hidden items-center gap-9 lg:flex">
{links.map((link) => {
const active = isActive(activeHref, link.href);
return (
<Link
aria-current={active ? "page" : undefined}
className={cn(
"relative py-1 text-sm transition-colors after:absolute after:inset-x-0 after:-bottom-0.5 after:h-px after:origin-left after:bg-current after:transition-transform after:duration-300 hover:after:scale-x-100",
// The underline is the active state, so it starts drawn.
active ? "after:scale-x-100" : "after:scale-x-0",
solid
? active
? "text-primary"
: "text-muted-foreground hover:text-primary"
: active
? "text-white"
: "text-white/80 hover:text-white",
)}
href={link.href}
key={link.href}
>
{link.label}
</Link>
);
})}
</nav>
<div className="flex items-center gap-3">
{phone && (
<a
className={cn(
"hidden items-center gap-2 text-sm transition-colors xl:flex",
solid
? "text-muted-foreground hover:text-primary"
: "text-white/80 hover:text-white",
)}
href={telHref(phone)}
>
<Phone className="size-4" />
{phone}
</a>
)}
{cta && (
<Button
className={cn(
"hidden border-none px-6 shadow-none sm:inline-flex",
solid
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "bg-white text-primary hover:bg-white/90",
)}
render={<Link href={cta.href} />}
size="lg"
>
{cta.label}
</Button>
)}
<Sheet>
<SheetTrigger
aria-label="Open menu"
className={cn(
"border-none bg-transparent shadow-none lg:hidden",
solid
? "text-primary hover:bg-muted"
: "text-white hover:bg-white/10",
)}
render={<Button size="icon-lg" variant="ghost" />}
>
<Menu className="size-5" />
</SheetTrigger>
<SheetPopup
className="w-[86%] max-w-sm border-border bg-background"
showCloseButton={false}
side="right"
>
<div className="flex items-center justify-between border-border border-b px-6 py-5">
<span className="font-display text-2xl text-primary leading-none">
{brand.name}
</span>
<SheetClose
aria-label="Close menu"
className="text-muted-foreground"
render={<Button size="icon" variant="ghost" />}
>
<X className="size-5" />
</SheetClose>
</div>
<nav className="flex flex-col divide-y divide-border">
{links.map((link) => {
const active = isActive(activeHref, link.href);
return (
<SheetClose
aria-current={active ? "page" : undefined}
className={cn(
"px-6 py-4 font-display text-2xl text-primary transition-colors hover:bg-muted",
active && "border-secondary border-l-2 bg-muted",
)}
key={link.href}
nativeButton={false}
render={<Link href={link.href} />}
>
{link.label}
</SheetClose>
);
})}
</nav>
<div className="mt-auto space-y-4 border-border border-t p-6">
{phone && (
<a
className="flex items-center gap-2 text-muted-foreground text-sm"
href={telHref(phone)}
>
<Phone className="size-4" />
{phone}
</a>
)}
{cta && (
<Button
className="w-full border-none bg-primary px-6 text-primary-foreground shadow-none hover:bg-primary/90"
render={<Link href={cta.href} />}
size="lg"
>
{cta.label}
</Button>
)}
</div>
</SheetPopup>
</Sheet>
</div>
</div>
</header>
);
}