Skip to content
goodtabs UI
BrowseBrowse components

Navbar

A site header with logo, links and actions that folds into a sheet on small screens.

npx shadcn@latest add https://ui.goodtabs.co/r/navbar.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/navbar

Three links and a primary button. Resize to see the menu.

With actions

Icon buttons and an avatar on the right, four links in the middle.

Minimal

A logo and one action, no links, so no menu button.

Usage

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

<Navbar
  logo={<Link href="/" className="font-semibold">Acme</Link>}
  links={[
    { href: "/product", label: "Product", active: true },
    { href: "/pricing", label: "Pricing" },
  ]}
  actions={<Button size="sm">Start free</Button>}
/>

Props

PropTypeDefaultDescription
logoReactNodeWordmark or logo, usually a link to the home page.
links{ href: string; label: string; active?: boolean }[][]Primary links. Hidden behind a menu button under md.
actionsReactNodeRight side: buttons, theme toggle, avatar.
menuLabelstring"Menu"Accessible name of the mobile menu button and sheet.
stickybooleantrueSticks to the top of its scroll container.

Source

navbar/navbar.tsx
"use client";

import * as React from "react";
import Link from "next/link";
import { MenuIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { cn } from "@/lib/utils";

export type NavbarLink = {
  href: string;
  label: string;
  /** Marks the current page. Sets aria-current and the foreground colour. */
  active?: boolean;
};

type NavbarProps = Omit<React.ComponentProps<"header">, "children"> & {
  /** Wordmark or logo, usually a link to the home page. */
  logo: React.ReactNode;
  /** Primary links. Hidden behind a menu button under md. */
  links?: NavbarLink[];
  /** Right side: buttons, theme toggle, avatar. */
  actions?: React.ReactNode;
  /** Accessible name of the mobile menu. */
  menuLabel?: string;
  /** Sticks to the top of its scroll container. */
  sticky?: boolean;
};

/**
 * A site header: logo left, links centre, actions right. Under md the links move into a sheet.
 * 56px tall, hairline bottom, blurred background so content can scroll under it.
 */
export function Navbar({
  logo,
  links = [],
  actions,
  menuLabel = "Menu",
  sticky = true,
  className,
  ...props
}: NavbarProps) {
  const [open, setOpen] = React.useState(false);

  return (
    <header
      data-slot="navbar"
      className={cn(
        "z-20 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80",
        sticky && "sticky top-0",
        className,
      )}
      {...props}
    >
      <div className="mx-auto flex h-14 max-w-6xl items-center gap-3 px-4 sm:gap-6 md:px-6">
        <div className="flex shrink-0 items-center whitespace-nowrap">{logo}</div>

        {links.length > 0 && (
          <nav aria-label="Primary" className="hidden items-center gap-1 md:flex">
            {links.map((link) => (
              <NavLink key={`${link.href}-${link.label}`} link={link} />
            ))}
          </nav>
        )}

        <div className="ml-auto flex items-center gap-2">
          {actions}
          {links.length > 0 && (
            <Sheet open={open} onOpenChange={setOpen}>
              <SheetTrigger asChild>
                <Button variant="ghost" size="icon-sm" className="md:hidden" aria-label={menuLabel}>
                  <MenuIcon />
                </Button>
              </SheetTrigger>
              <SheetContent side="right" className="w-72 gap-0 p-0">
                <SheetHeader className="h-14 justify-center border-b px-4">
                  <SheetTitle className="text-sm">{menuLabel}</SheetTitle>
                  <SheetDescription className="sr-only">Site links</SheetDescription>
                </SheetHeader>
                <nav aria-label="Primary, mobile" className="flex flex-col gap-px p-2">
                  {links.map((link) => (
                    <NavLink key={`${link.href}-${link.label}`} link={link} stacked onNavigate={() => setOpen(false)} />
                  ))}
                </nav>
              </SheetContent>
            </Sheet>
          )}
        </div>
      </div>
    </header>
  );
}

function NavLink({ link, stacked, onNavigate }: { link: NavbarLink; stacked?: boolean; onNavigate?: () => void }) {
  return (
    <Link
      href={link.href}
      aria-current={link.active ? "page" : undefined}
      onClick={onNavigate}
      className={cn(
        "rounded-sm text-sm transition-colors duration-100 ease-out hover:bg-muted hover:text-foreground",
        stacked ? "flex h-9 items-center px-2" : "px-2 py-1",
        link.active ? "font-medium text-foreground" : "text-muted-foreground",
        stacked && link.active && "bg-muted",
      )}
    >
      {link.label}
    </Link>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/navbar/prompt.md, or the Copy AI prompt button above.

When to use

  • The top of a marketing site or a small app with up to five primary links
  • When the header should stay in view while content scrolls under it
  • When you want one component for desktop links and the mobile menu

When not to use

  • For apps with deep navigation, use a sidebar
  • For nested menus and mega menus, compose navigation-menu instead
  • Inside a page section: it is page chrome, not content

Agent notes

  • Links use next/link, so pass real hrefs; set active yourself from the pathname
  • The sheet only renders when links is non-empty, so a logo plus actions header has no menu button
  • It is a client component because of the sheet state; the logo and actions you pass can be server rendered
  • Pass sticky={false} when rendering it inside a demo or a scroll container with its own header