# Navbar with mobile menu

> Sticky navigation bar that turns translucent on scroll, with an accessible mobile menu.

A shadcn/ui block from the [@balick](https://ui.balick.me) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/balick/navbar-01).

- Page, with a live preview: https://shadcn.exchange/r/balick/navbar-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/balick/navbar-01-light-77820194dc15e437.png
- Registry item: https://ui.balick.me/r/navbar-01.json
- Categories: navbar
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.balick.me/r/navbar-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `https://ui.balick.me/r/section.json`

## Files

### `registry/balick/blocks/navbar-01/navbar-01.tsx`

```tsx
"use client"

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

import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { Container } from "@/registry/balick/ui/section"

const brand = { name: "Acme", href: "#" }

const links = [
  { label: "Features", href: "#" },
  { label: "Pricing", href: "#" },
  { label: "Customers", href: "#" },
  { label: "Changelog", href: "#" },
  { label: "Docs", href: "#" },
]

const actions = {
  secondary: { label: "Sign in", href: "#" },
  primary: { label: "Get started", href: "#" },
}

/** True once the page has scrolled past `offset` pixels. */
function useScrolled(offset = 8) {
  const [scrolled, setScrolled] = React.useState(false)

  React.useEffect(() => {
    const update = () => setScrolled(window.scrollY > offset)
    update()
    window.addEventListener("scroll", update, { passive: true })
    return () => window.removeEventListener("scroll", update)
  }, [offset])

  return scrolled
}

export function Navbar01() {
  const [open, setOpen] = React.useState(false)
  const scrolled = useScrolled()
  const menuId = React.useId()

  React.useEffect(() => {
    if (!open) return
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") setOpen(false)
    }
    // The menu only exists below the md breakpoint.
    const desktop = window.matchMedia("(min-width: 768px)")
    const onResize = () => desktop.matches && setOpen(false)
    document.addEventListener("keydown", onKeyDown)
    desktop.addEventListener("change", onResize)
    return () => {
      document.removeEventListener("keydown", onKeyDown)
      desktop.removeEventListener("change", onResize)
    }
  }, [open])

  return (
    <header
      className={cn(
        "sticky top-0 z-50 w-full border-b transition-colors duration-300",
        scrolled || open
          ? "border-border bg-background/80 backdrop-blur-md supports-[backdrop-filter]:bg-background/60"
          : "border-transparent bg-transparent"
      )}
    >
      <Container className="flex h-16 items-center gap-8">
        <a
          href={brand.href}
          className="flex items-center gap-2 font-semibold tracking-tight"
        >
          <Triangle className="size-5 fill-current" />
          {brand.name}
        </a>

        <nav aria-label="Main" className="hidden md:block">
          <ul className="flex items-center gap-1">
            {links.map((link) => (
              <li key={link.label}>
                <a
                  href={link.href}
                  className="rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
                >
                  {link.label}
                </a>
              </li>
            ))}
          </ul>
        </nav>

        <div className="ml-auto hidden items-center gap-2 md:flex">
          <Button variant="ghost" size="sm" asChild>
            <a href={actions.secondary.href}>{actions.secondary.label}</a>
          </Button>
          <Button size="sm" asChild>
            <a href={actions.primary.href}>{actions.primary.label}</a>
          </Button>
        </div>

        <Button
          variant="ghost"
          size="icon"
          aria-label={open ? "Close menu" : "Open menu"}
          aria-expanded={open}
          aria-controls={menuId}
          onClick={() => setOpen(!open)}
          className="ml-auto md:hidden"
        >
          {open ? <X className="size-5" /> : <Menu className="size-5" />}
        </Button>
      </Container>

      <div
        id={menuId}
        inert={!open}
        className={cn(
          "grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none md:hidden",
          open ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
        )}
      >
        <div className="overflow-hidden">
          <Container className="flex flex-col gap-6 pt-2 pb-6">
            <nav aria-label="Mobile">
              <ul className="flex flex-col">
                {links.map((link) => (
                  <li key={link.label}>
                    <a
                      href={link.href}
                      onClick={() => setOpen(false)}
                      className="block border-b py-3 text-base text-muted-foreground transition-colors hover:text-foreground"
                    >
                      {link.label}
                    </a>
                  </li>
                ))}
              </ul>
            </nav>
            <div className="grid grid-cols-1 gap-2">
              <Button variant="outline" asChild>
                <a href={actions.secondary.href}>{actions.secondary.label}</a>
              </Button>
              <Button asChild>
                <a href={actions.primary.href}>{actions.primary.label}</a>
              </Button>
            </div>
          </Container>
        </div>
      </div>
    </header>
  )
}
```
