Hover Navbar
Responsive navbar with a sliding hover pill and animated mobile menu.
components/ui/hover-navbar.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { Hexagon, Menu, X } from "lucide-react";
import { useId, useState } from "react";
type NavLink = { label: string; href: string };
const defaultLinks: NavLink[] = [
{ label: "Product", href: "#" },
{ label: "Solutions", href: "#" },
{ label: "Pricing", href: "#" },
{ label: "Customers", href: "#" },
{ label: "Docs", href: "#" },
];
export function HoverNavbar({ brand = "Acme", links = defaultLinks }: { brand?: string; links?: NavLink[] }) {
const [hovered, setHovered] = useState<string | null>(null);
const [open, setOpen] = useState(false);
const pillId = useId();
return (
<header className="relative w-full rounded-2xl border border-zinc-200 bg-white/80 px-4 py-2.5 shadow-sm backdrop-blur-md">
<div className="flex items-center justify-between gap-4">
<a href="#" className="flex items-center gap-2 font-semibold text-zinc-900">
<Hexagon className="size-5 fill-zinc-900" />
{brand}
</a>
<nav className="hidden md:block" onMouseLeave={() => setHovered(null)}>
<ul className="flex items-center">
{links.map((link) => (
<li key={link.label}>
<a
href={link.href}
onMouseEnter={() => setHovered(link.label)}
className="relative block px-3.5 py-2 text-sm text-zinc-600 transition-colors hover:text-zinc-900"
>
{hovered === link.label && (
<motion.span
layoutId={pillId}
className="absolute inset-0 rounded-lg bg-zinc-100"
transition={{ type: "spring", stiffness: 400, damping: 34 }}
/>
)}
<span className="relative">{link.label}</span>
</a>
</li>
))}
</ul>
</nav>
<div className="flex items-center gap-2">
<a href="#" className="hidden px-3 py-2 text-sm text-zinc-600 hover:text-zinc-900 sm:block">
Sign in
</a>
<a href="#" className="rounded-lg bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-zinc-800">
Get started
</a>
<button
aria-label={open ? "Close menu" : "Open menu"}
onClick={() => setOpen((v) => !v)}
className="grid size-9 place-items-center rounded-lg text-zinc-700 hover:bg-zinc-100 md:hidden"
>
{open ? <X className="size-5" /> : <Menu className="size-5" />}
</button>
</div>
</div>
<AnimatePresence>
{open && (
<motion.nav
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="overflow-hidden md:hidden"
>
<ul className="space-y-1 pb-2 pt-3">
{links.map((link, i) => (
<motion.li key={link.label} initial={{ opacity: 0, x: -8 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: i * 0.04 }}>
<a href={link.href} className="block rounded-lg px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100">
{link.label}
</a>
</motion.li>
))}
</ul>
</motion.nav>
)}
</AnimatePresence>
</header>
);
}
export default function Demo() {
return (
<div className="w-full max-w-4xl">
<HoverNavbar />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/hover-navbar.tsxcomponents/ui/hover-navbar.tsx"use client"; import { AnimatePresence, motion } from "motion/react"; import { Hexagon, Menu, X } from "lucide-react"; import { useId, useState } from "react"; type NavLink = { label: string; href: string }; const defaultLinks: NavLink[] = [ { label: "Product", href: "#" }, { label: "Solutions", href: "#" }, { label: "Pricing", href: "#" }, { label: "Customers", href: "#" }, { label: "Docs", href: "#" }, ]; export function HoverNavbar({ brand = "Acme", links = defaultLinks }: { brand?: string; links?: NavLink[] }) { const [hovered, setHovered] = useState<string | null>(null); const [open, setOpen] = useState(false); const pillId = useId(); return ( <header className="relative w-full rounded-2xl border border-zinc-200 bg-white/80 px-4 py-2.5 shadow-sm backdrop-blur-md"> <div className="flex items-center justify-between gap-4"> <a href="#" className="flex items-center gap-2 font-semibold text-zinc-900"> <Hexagon className="size-5 fill-zinc-900" /> {brand} </a> <nav className="hidden md:block" onMouseLeave={() => setHovered(null)}> <ul className="flex items-center"> {links.map((link) => ( <li key={link.label}> <a href={link.href} onMouseEnter={() => setHovered(link.label)} className="relative block px-3.5 py-2 text-sm text-zinc-600 transition-colors hover:text-zinc-900" > {hovered === link.label && ( <motion.span layoutId={pillId} className="absolute inset-0 rounded-lg bg-zinc-100" transition={{ type: "spring", stiffness: 400, damping: 34 }} /> )} <span className="relative">{link.label}</span> </a> </li> ))} </ul> </nav> <div className="flex items-center gap-2"> <a href="#" className="hidden px-3 py-2 text-sm text-zinc-600 hover:text-zinc-900 sm:block"> Sign in </a> <a href="#" className="rounded-lg bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-zinc-800"> Get started </a> <button aria-label={open ? "Close menu" : "Open menu"} onClick={() => setOpen((v) => !v)} className="grid size-9 place-items-center rounded-lg text-zinc-700 hover:bg-zinc-100 md:hidden" > {open ? <X className="size-5" /> : <Menu className="size-5" />} </button> </div> </div> <AnimatePresence> {open && ( <motion.nav initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }} className="overflow-hidden md:hidden" > <ul className="space-y-1 pb-2 pt-3"> {links.map((link, i) => ( <motion.li key={link.label} initial={{ opacity: 0, x: -8 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: i * 0.04 }}> <a href={link.href} className="block rounded-lg px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100"> {link.label} </a> </motion.li> ))} </ul> </motion.nav> )} </AnimatePresence> </header> ); } export default function Demo() { return ( <div className="w-full max-w-4xl"> <HoverNavbar /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.