shadimage

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