# Navbar

> Responsive navbar with frosted glass on scroll, a mobile menu and a floating pill variant.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/navbar
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/navbar.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/navbar-demo.tsx"
import { Hexagon } from "lucide-react"

import { Navbar } from "@/components/navbar"

export default function NavbarDemo() {
  return (
    <div className="min-h-[200svh] bg-[radial-gradient(60rem_30rem_at_50%_-10%,rgb(249_115_22/0.18),transparent)]">
      <Navbar
        brand={
          <>
            <Hexagon className="size-5 fill-primary text-primary" /> Hexa
          </>
        }
        links={[
          { label: "Product", href: "#", active: true },
          { label: "Pricing", href: "#" },
          { label: "Customers", href: "#" },
          { label: "Changelog", href: "#" },
          { label: "Docs", href: "#" },
        ]}
        secondaryAction={{ label: "Sign in", href: "#" }}
        primaryAction={{ label: "Get started", href: "#" }}
      />
      <section className="mx-auto flex max-w-3xl flex-col items-center gap-5 px-6 pt-28 text-center">
        <span className="rounded-full border bg-background/60 px-3 py-1 text-xs text-muted-foreground">v2.0 is live</span>
        <h1 className="text-5xl font-semibold tracking-tight text-balance">Ship interfaces your users remember</h1>
        <p className="max-w-xl text-muted-foreground">
          Scroll down — the bar turns into frosted glass. Shrink the window to see the mobile menu.
        </p>
      </section>
      <div className="mx-auto mt-20 grid max-w-5xl grid-cols-3 gap-4 px-6">
        {Array.from({ length: 6 }, (_, i) => (
          <div key={i} className="h-40 rounded-2xl border bg-muted/40" />
        ))}
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/navbar.tsx"
"use client"

import * as React from "react"
import { Menu, X } from "lucide-react"

import { cn } from "@/lib/utils"

export interface NavbarLink {
  label: string
  href: string
  active?: boolean
}

export interface NavbarProps extends React.ComponentProps<"header"> {
  brand: React.ReactNode
  links: NavbarLink[]
  primaryAction?: { label: string; href: string }
  secondaryAction?: { label: string; href: string }
  /** "bar" spans the full width, "floating" is an inset pill */
  variant?: "bar" | "floating"
  sticky?: boolean
  /** Frosted glass background once the page is scrolled */
  blur?: boolean
}

function subscribeScroll(onChange: () => void) {
  window.addEventListener("scroll", onChange, { passive: true })
  return () => window.removeEventListener("scroll", onChange)
}

const isScrolled = () => window.scrollY > 8

export function Navbar({
  brand,
  links,
  primaryAction,
  secondaryAction,
  variant = "bar",
  sticky = true,
  blur = true,
  className,
  ...props
}: NavbarProps) {
  const [open, setOpen] = React.useState(false)
  const scrolled = React.useSyncExternalStore(subscribeScroll, isScrolled, () => false)
  const menuId = React.useId()

  React.useEffect(() => {
    if (!open) return
    const onKey = (event: KeyboardEvent) => event.key === "Escape" && setOpen(false)
    const onResize = () => window.innerWidth >= 768 && setOpen(false)
    window.addEventListener("keydown", onKey)
    window.addEventListener("resize", onResize)
    return () => {
      window.removeEventListener("keydown", onKey)
      window.removeEventListener("resize", onResize)
    }
  }, [open])

  const floating = variant === "floating"
  const raised = scrolled || open

  return (
    <header
      className={cn("inset-x-0 top-0 z-40", sticky && "sticky", floating && "px-4 pt-3", className)}
      {...props}
    >
      <div
        className={cn(
          "mx-auto transition-[background-color,border-color,box-shadow] duration-300",
          floating ? "max-w-5xl rounded-2xl border" : "border-b",
          raised
            ? cn(blur ? "bg-background/75 backdrop-blur-xl" : "bg-background", "shadow-[0_8px_30px_-12px_rgb(0_0_0/0.15)]")
            : floating
              ? "bg-background/60"
              : "border-transparent bg-transparent"
        )}
      >
        <nav className={cn("mx-auto flex h-14 items-center gap-6 px-4", !floating && "max-w-6xl sm:px-6")}>
          <a href="#" className="flex shrink-0 items-center gap-2 font-semibold">
            {brand}
          </a>
          <ul className="hidden flex-1 items-center gap-1 md:flex">
            {links.map((link) => (
              <li key={link.label}>
                <a
                  href={link.href}
                  aria-current={link.active ? "page" : undefined}
                  className="rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground aria-[current=page]:font-medium aria-[current=page]:text-foreground"
                >
                  {link.label}
                </a>
              </li>
            ))}
          </ul>
          <div className="ml-auto hidden items-center gap-2 md:flex">
            {secondaryAction && (
              <a href={secondaryAction.href} className="rounded-md px-3 py-1.5 text-sm font-medium hover:bg-accent">
                {secondaryAction.label}
              </a>
            )}
            {primaryAction && (
              <a
                href={primaryAction.href}
                className="rounded-lg bg-primary px-3.5 py-1.5 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90"
              >
                {primaryAction.label}
              </a>
            )}
          </div>
          <button
            type="button"
            aria-expanded={open}
            aria-controls={menuId}
            aria-label={open ? "Close menu" : "Open menu"}
            onClick={() => setOpen((value) => !value)}
            className="ml-auto flex size-9 items-center justify-center rounded-md hover:bg-accent md:hidden"
          >
            {open ? <X className="size-5" /> : <Menu className="size-5" />}
          </button>
        </nav>
        <div
          id={menuId}
          inert={!open}
          className={cn("grid transition-[grid-template-rows] duration-300 md:hidden", open ? "grid-rows-[1fr]" : "grid-rows-[0fr]")}
        >
          <div className="overflow-hidden">
            <ul className="flex flex-col gap-1 border-t px-4 py-3">
              {links.map((link) => (
                <li key={link.label}>
                  <a
                    href={link.href}
                    onClick={() => setOpen(false)}
                    aria-current={link.active ? "page" : undefined}
                    className="block rounded-md px-3 py-2 text-sm hover:bg-accent aria-[current=page]:font-medium"
                  >
                    {link.label}
                  </a>
                </li>
              ))}
            </ul>
            {(primaryAction || secondaryAction) && (
              <div className="flex gap-2 px-4 pb-4">
                {secondaryAction && (
                  <a href={secondaryAction.href} className="flex-1 rounded-lg border px-3 py-2 text-center text-sm font-medium">
                    {secondaryAction.label}
                  </a>
                )}
                {primaryAction && (
                  <a
                    href={primaryAction.href}
                    className="flex-1 rounded-lg bg-primary px-3 py-2 text-center text-sm font-medium text-primary-foreground"
                  >
                    {primaryAction.label}
                  </a>
                )}
              </div>
            )}
          </div>
        </div>
      </div>
    </header>
  )
}
```
