Navbar
A floating navbar that shrinks and collapses to a pill shape on scroll.
components/library/navbar.tsx
"use client";
import { motion } from "motion/react";
import Link from "next/link";
import { useEffect, useState } from "react";
/** A single navigation link entry. */
export interface NavItem {
label: string;
href: string;
/** React node rendered before the label (icon, SVG, Image, etc.). */
icon?: React.ReactNode;
}
export interface NavbarProps {
/**
* Brand / logo item on the left.
* @default { label: "Knot-ui", href: "/" }
*/
logo?: NavItem;
/**
* Label for the login / CTA link on the right.
* @default "Login"
*/
loginLabel?: string;
/**
* href for the login / CTA link.
* @default "/login"
*/
loginHref?: string;
/**
* Width when at the top of the page (fully expanded).
* @default "min(56rem, calc(100vw - 2rem))"
*/
expandedWidth?: string;
/**
* Width after scrolling past `shrinkAfter`. Corners also animate to a full
* pill shape at this point.
* @default "min(18rem, calc(100vw - 2rem))"
*/
collapsedWidth?: string;
/**
* Scroll distance in pixels before the navbar fully collapses.
* @default 200
*/
shrinkAfter?: number;
className?: string;
}
export const Navbar = ({
logo = { label: "Knot-ui", href: "/", icon: <img src="/home.svg" width={25} height={25} alt="" /> },
loginLabel = "Login",
loginHref = "/login",
expandedWidth = "min(56rem, calc(100vw - 2rem))",
collapsedWidth = "min(18rem, calc(100vw - 2rem))",
shrinkAfter = 200,
className = "",
}: NavbarProps) => {
const [isCollapsed, setIsCollapsed] = useState(false);
useEffect(() => {
const onScroll = () => setIsCollapsed(window.scrollY >= shrinkAfter);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, [shrinkAfter]);
return (
<div className={`fixed top-0 left-1/2 -translate-x-1/2 mt-5 z-50 ${className}`}>
<motion.nav
animate={{
width: isCollapsed ? collapsedWidth : expandedWidth,
}}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
aria-label="Site navigation"
className="bg-gray-100 px-5 rounded-xl py-1 flex justify-between items-center shadow-sm overflow-hidden"
>
{/* Logo / brand */}
<Link
href={logo.href}
className="inline-flex items-center gap-2 px-3 py-2 whitespace-nowrap"
>
{logo.icon && <span className="shrink-0">{logo.icon}</span>}
<span className="text-base font-semibold text-neutral-900 tracking-tight">
{logo.label}
</span>
</Link>
{/* Login / CTA */}
<Link
href={loginHref}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-neutral-500 hover:text-neutral-950 transition-colors whitespace-nowrap"
>
{loginLabel}
</Link>
</motion.nav>
</div>
);
};
Navbar.displayName = "Navbar";- Where it goes
- navbar
- Interaction
- accordion
DependenciesBundle weight · medium
- animation
- motion