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
With actions
Minimal
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
| Prop | Type | Default | Description |
|---|---|---|---|
| logo | ReactNode | Wordmark 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. |
| actions | ReactNode | Right side: buttons, theme toggle, avatar. | |
| menuLabel | string | "Menu" | Accessible name of the mobile menu button and sheet. |
| sticky | boolean | true | Sticks 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