# Navbar

A site header that renders a navigation menu on wide screens and a fullscreen menu when space runs out.

Navbar is an open-source pattern from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/patterns/navbar

**React**

**Example**

```tsx
import {
  BookOpenIcon,
  ChartLineIcon,
  CodeIcon,
  LifebuoyIcon,
  LightningIcon,
  ShieldCheckIcon,
} from "@phosphor-icons/react"
import { Navbar, type NavbarItem } from "@/components/patterns/navbar"

const items: NavbarItem[] = [
  {
    label: "Products",
    items: [
      {
        label: "Analytics",
        href: "#analytics",
        description: "Track traffic and conversions in real time.",
        icon: (
          <ChartLineIcon className="mt-0.5" />
        ),
      },
      {
        label: "Automations",
        href: "#automations",
        description: "Run workflows when events happen.",
        icon: (
          <LightningIcon className="mt-0.5" />
        ),
      },
      {
        label: "Security",
        href: "#security",
        description: "Single sign-on, audit logs, and roles.",
        icon: (
          <ShieldCheckIcon className="mt-0.5" />
        ),
      },
    ],
  },
  {
    label: "Resources",
    items: [
      {
        label: "Documentation",
        href: "#docs",
        icon: (
          <BookOpenIcon />
        ),
      },
      {
        label: "API reference",
        href: "#api",
        icon: (
          <CodeIcon />
        ),
      },
      {
        label: "Help center",
        href: "#help",
        icon: (
          <LifebuoyIcon />
        ),
      },
    ],
  },
  { label: "Pricing", href: "#pricing" },
  { label: "Blog", href: "#blog" },
]

const brand = <span className="text-base font-semibold">Acme</span>

export default function NavbarDemo() {
  return (
    <div className="min-h-96">
      <Navbar items={items} brand={brand} />
    </div>
  )
}
```

`Navbar` builds a `header` from an `items` array. On wide screens it renders a `NavigationMenu`: plain links appear inline, groups open a dropdown of links with optional icons and descriptions, and mega items open multi-column panels. The `brand` sits at the start and `actions` at the end.

The navbar switches to a compact layout below the `48rem` breakpoint, and also on wider screens whenever its contents overflow the header; it measures itself with a `ResizeObserver` and returns to the full menu once there is room again. In the compact layout the items move into a `FullscreenMenu` opened by a menu button, with plain links in the first group and each group or mega column as a labelled section. Use it as the primary navigation of a marketing site or documentation site.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/navbar
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add patterns navbar
```

### Manual

1. Add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper).

2. Add the pieces it builds on: [Fullscreen Menu](https://ui.suiss.app/docs/components/fullscreen-menu), [Navigation Menu](https://ui.suiss.app/docs/components/navigation-menu), `use-mobile`.

3. Copy the source into `components/patterns/navbar.tsx`:

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

import * as React from "react"
import {
  FullscreenMenu,
  FullscreenMenuContent,
  FullscreenMenuGroup,
  FullscreenMenuLabel,
  FullscreenMenuLink,
  FullscreenMenuTitle,
  FullscreenMenuTrigger,
} from "@/components/ui/fullscreen-menu"
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"

type NavbarLink = {
  label: string
  href: string
  description?: string
  icon?: React.ReactNode
  active?: boolean
}

type NavbarColumn = {
  label: string
  links: NavbarLink[]
  featured?: boolean
}

type NavbarGroup = { label: string; items: NavbarLink[] }

type NavbarMega = { label: string; columns: NavbarColumn[] }

type NavbarItem = NavbarLink | NavbarGroup | NavbarMega

function isGroup(item: NavbarItem): item is NavbarGroup {
  return "items" in item
}

function isMega(item: NavbarItem): item is NavbarMega {
  return "columns" in item
}

function NavbarAnchor(props: React.ComponentProps<"a">) {
  return <a {...props} />
}

function NavbarMenuItem({
  item,
  render,
}: {
  item: NavbarItem
  render: React.ReactElement
}) {
  if (isMega(item)) {
    return (
      <NavigationMenuItem value={item.label}>
        <NavigationMenuTrigger>{item.label}</NavigationMenuTrigger>
        <NavigationMenuContent className="w-full p-0">
          <div
            data-slot="navbar-columns"
            className="mx-auto flex max-w-5xl gap-16 px-6 pt-7 pb-14"
          >
            {item.columns.map((column, index) => (
              <div
                key={column.label}
                data-slot="navbar-column"
                data-featured={column.featured ? "" : undefined}
                style={{ transitionDelay: `${60 + index * 40}ms` }}
                className="group/navbar-column flex flex-col gap-2 transition-[opacity,translate] duration-700 ease-[cubic-bezier(0.45,0,0.2,1)] starting:-translate-y-1.5 starting:opacity-0 motion-reduce:transition-none data-featured:gap-2.5"
              >
                <span className="text-xs text-muted-foreground">
                  {column.label}
                </span>
                {column.links.map((link) => (
                  <NavigationMenuLink
                    key={link.label}
                    href={link.href}
                    active={link.active}
                    render={render}
                    className="w-fit rounded-sm p-0 text-xs leading-tight font-semibold hover:bg-transparent hover:underline focus:bg-transparent group-data-featured/navbar-column:text-2xl group-data-featured/navbar-column:tracking-tight"
                  >
                    {link.label}
                  </NavigationMenuLink>
                ))}
              </div>
            ))}
          </div>
        </NavigationMenuContent>
      </NavigationMenuItem>
    )
  }

  if (isGroup(item)) {
    return (
      <NavigationMenuItem value={item.label}>
        <NavigationMenuTrigger>{item.label}</NavigationMenuTrigger>
        <NavigationMenuContent>
          <ul className="grid w-80 gap-1">
            {item.items.map((link) => (
              <li key={link.label}>
                <NavigationMenuLink
                  href={link.href}
                  active={link.active}
                  render={render}
                  className={cn(link.description && "items-start gap-3")}
                >
                  {link.icon}
                  {link.description ? (
                    <div className="flex flex-col gap-1">
                      <span className="font-medium">{link.label}</span>
                      <span className="text-muted-foreground">
                        {link.description}
                      </span>
                    </div>
                  ) : (
                    link.label
                  )}
                </NavigationMenuLink>
              </li>
            ))}
          </ul>
        </NavigationMenuContent>
      </NavigationMenuItem>
    )
  }

  return (
    <NavigationMenuItem>
      <NavigationMenuLink
        href={item.href}
        active={item.active}
        render={render}
        className={navigationMenuTriggerStyle()}
      >
        {item.label}
      </NavigationMenuLink>
    </NavigationMenuItem>
  )
}

function Navbar({
  items,
  brand,
  actions,
  menuLabel = "Menu",
  closeLabel,
  layout = "popover",
  render = <NavbarAnchor />,
  className,
  ref,
  ...props
}: React.ComponentProps<"header"> & {
  items: NavbarItem[]
  brand?: React.ReactNode
  actions?: React.ReactNode
  menuLabel?: string
  closeLabel?: string
  layout?: "popover" | "panel"
  render?: React.ReactElement
}) {
  const isMobile = useIsMobile()
  const headerRef = React.useRef<HTMLElement>(null)
  const [crowdedBelow, setCrowdedBelow] = React.useState<number | null>(null)
  const compact = isMobile || crowdedBelow !== null

  React.useLayoutEffect(() => {
    const header = headerRef.current
    if (!header || isMobile) return
    const measure = () => {
      if (crowdedBelow === null) {
        if (header.scrollWidth > header.clientWidth + 1) {
          setCrowdedBelow(header.scrollWidth)
        }
      } else if (header.clientWidth >= crowdedBelow) {
        setCrowdedBelow(null)
      }
    }
    measure()
    const observer = new ResizeObserver(measure)
    observer.observe(header)
    for (const child of header.children) observer.observe(child)
    return () => observer.disconnect()
  }, [crowdedBelow, isMobile])
  const links = items.filter(
    (item): item is NavbarLink => !isGroup(item) && !isMega(item)
  )
  const groups = items.flatMap((item) => {
    if (isGroup(item)) return [item]
    if (isMega(item)) {
      return item.columns.map((column) => ({
        label: column.label,
        items: column.links,
      }))
    }
    return []
  })

  return (
    <header
      ref={(node) => {
        headerRef.current = node
        if (typeof ref === "function") return ref(node)
        if (ref) ref.current = node
      }}
      data-slot="navbar"
      data-layout={layout}
      data-compact={compact ? "" : undefined}
      className={cn(
        "flex h-14 w-full items-center gap-4 px-4 data-[layout=panel]:relative data-[layout=panel]:z-50 md:px-6",
        className
      )}
      {...props}
    >
      {brand && (
        <div data-slot="navbar-brand" className="flex shrink-0 items-center">
          {brand}
        </div>
      )}
      {compact ? (
        <div className="ms-auto flex items-center gap-2">
          {actions}
          <FullscreenMenu>
            <FullscreenMenuTrigger aria-label={menuLabel} />
            <FullscreenMenuContent closeLabel={closeLabel}>
              <FullscreenMenuTitle>{menuLabel}</FullscreenMenuTitle>
              {links.length > 0 && (
                <FullscreenMenuGroup>
                  {links.map((link) => (
                    <FullscreenMenuLink
                      key={link.label}
                      href={link.href}
                      isActive={link.active}
                      render={render}
                    >
                      {link.label}
                    </FullscreenMenuLink>
                  ))}
                </FullscreenMenuGroup>
              )}
              {groups.map((group) => (
                <FullscreenMenuGroup key={group.label}>
                  <FullscreenMenuLabel>{group.label}</FullscreenMenuLabel>
                  {group.items.map((link) => (
                    <FullscreenMenuLink
                      key={link.label}
                      href={link.href}
                      isActive={link.active}
                      render={render}
                    >
                      {link.label}
                    </FullscreenMenuLink>
                  ))}
                </FullscreenMenuGroup>
              ))}
            </FullscreenMenuContent>
          </FullscreenMenu>
        </div>
      ) : (
        <>
          <NavigationMenu
            className="mx-auto"
            layout={layout}
            anchor={layout === "panel" ? headerRef : undefined}
          >
            <NavigationMenuList>
              {items.map((item) => (
                <NavbarMenuItem key={item.label} item={item} render={render} />
              ))}
            </NavigationMenuList>
          </NavigationMenu>
          {actions && (
            <div
              data-slot="navbar-actions"
              className="flex shrink-0 items-center gap-2"
            >
              {actions}
            </div>
          )}
        </>
      )}
    </header>
  )
}

export { Navbar, type NavbarColumn, type NavbarItem, type NavbarLink }
```

4. Update the import paths to match your project.

## Usage

```tsx
import { Navbar } from "@/components/patterns/navbar"
```

```tsx
<div className="min-h-96">
  <Navbar items={items} brand={brand} />
</div>
```

Each `NavbarItem` is one of three shapes: a link with `label` and `href` (plus optional `description`, `icon` and `active`), a group with `label` and `items`, or a mega item with `label` and `columns`. Each column has a `label`, `links` and an optional `featured` flag that sets its links in a large type size. Pass `render` to route links through your framework's link component; it receives `href` and the link content.

## Examples

### With actions

Adds a Sign in button at the end of the header with `actions`.

**Example**

```tsx
import {
  BookOpenIcon,
  ChartLineIcon,
  CodeIcon,
  LifebuoyIcon,
  LightningIcon,
  ShieldCheckIcon,
} from "@phosphor-icons/react"
import { Navbar, type NavbarItem } from "@/components/patterns/navbar"
import { Button } from "@/components/ui/button"

const items: NavbarItem[] = [
  {
    label: "Products",
    items: [
      {
        label: "Analytics",
        href: "#analytics",
        description: "Track traffic and conversions in real time.",
        icon: (
          <ChartLineIcon className="mt-0.5" />
        ),
      },
      {
        label: "Automations",
        href: "#automations",
        description: "Run workflows when events happen.",
        icon: (
          <LightningIcon className="mt-0.5" />
        ),
      },
      {
        label: "Security",
        href: "#security",
        description: "Single sign-on, audit logs, and roles.",
        icon: (
          <ShieldCheckIcon className="mt-0.5" />
        ),
      },
    ],
  },
  {
    label: "Resources",
    items: [
      {
        label: "Documentation",
        href: "#docs",
        icon: (
          <BookOpenIcon />
        ),
      },
      {
        label: "API reference",
        href: "#api",
        icon: (
          <CodeIcon />
        ),
      },
      {
        label: "Help center",
        href: "#help",
        icon: (
          <LifebuoyIcon />
        ),
      },
    ],
  },
  { label: "Pricing", href: "#pricing" },
  { label: "Blog", href: "#blog" },
]

const brand = <span className="text-base font-semibold">Acme</span>

export default function NavbarWithActions() {
  return (
    <div className="min-h-96">
      <Navbar
        items={items}
        brand={brand}
        actions={
          <Button size="sm" variant="secondary">
            Sign in
          </Button>
        }
      />
    </div>
  )
}
```

### Links only

Uses only plain links and marks the current page with `active`.

**Example**

```tsx
import { Navbar } from "@/components/patterns/navbar"

const brand = <span className="text-base font-semibold">Acme</span>

export default function NavbarLinksOnly() {
  return (
    <div className="min-h-96">
      <Navbar
        items={[
          { label: "Overview", href: "#overview", active: true },
          { label: "Customers", href: "#customers" },
          { label: "Changelog", href: "#changelog" },
          { label: "Blog", href: "#blog" },
        ]}
        brand={brand}
      />
    </div>
  )
}
```

### Crowded

Shows nine links in a narrow frame, folding them into the menu until the frame is wide enough.

**Example**

```tsx
import { Navbar } from "@/components/patterns/navbar"
import { Button } from "@/components/ui/button"

const brand = <span className="text-base font-semibold">Acme</span>

export default function NavbarCrowded() {
  return (
    <div className="min-h-96">
      <div data-testid="frame" className="w-160 max-w-full">
        <Navbar
          items={[
            "Store",
            "Laptop",
            "Tablet",
            "Phone",
            "Watch",
            "Headphones",
            "Speakers",
            "Accessories",
            "Support",
          ].map((label) => ({ label, href: `#${label.toLowerCase()}` }))}
          brand={brand}
          actions={
            <Button size="sm" variant="secondary">
              Sign in
            </Button>
          }
        />
      </div>
    </div>
  )
}
```

### With close label

Labels the fullscreen menu's close button Done with `closeLabel`.

**Example**

```tsx
import {
  BookOpenIcon,
  ChartLineIcon,
  CodeIcon,
  LifebuoyIcon,
  LightningIcon,
  ShieldCheckIcon,
} from "@phosphor-icons/react"
import { Navbar, type NavbarItem } from "@/components/patterns/navbar"

const items: NavbarItem[] = [
  {
    label: "Products",
    items: [
      {
        label: "Analytics",
        href: "#analytics",
        description: "Track traffic and conversions in real time.",
        icon: (
          <ChartLineIcon className="mt-0.5" />
        ),
      },
      {
        label: "Automations",
        href: "#automations",
        description: "Run workflows when events happen.",
        icon: (
          <LightningIcon className="mt-0.5" />
        ),
      },
      {
        label: "Security",
        href: "#security",
        description: "Single sign-on, audit logs, and roles.",
        icon: (
          <ShieldCheckIcon className="mt-0.5" />
        ),
      },
    ],
  },
  {
    label: "Resources",
    items: [
      {
        label: "Documentation",
        href: "#docs",
        icon: (
          <BookOpenIcon />
        ),
      },
      {
        label: "API reference",
        href: "#api",
        icon: (
          <CodeIcon />
        ),
      },
      {
        label: "Help center",
        href: "#help",
        icon: (
          <LifebuoyIcon />
        ),
      },
    ],
  },
  { label: "Pricing", href: "#pricing" },
  { label: "Blog", href: "#blog" },
]

const brand = <span className="text-base font-semibold">Acme</span>

export default function NavbarWithCloseLabel() {
  return (
    <div className="min-h-96">
      <Navbar items={items} brand={brand} closeLabel="Done" />
    </div>
  )
}
```

### Mega panel

Uses `layout="panel"` with a mega item whose columns open in a full-width panel.

**Example**

```tsx
import { Navbar } from "@/components/patterns/navbar"

const brand = <span className="text-base font-semibold">Acme</span>

export default function NavbarMegaPanel() {
  return (
    <div className="min-h-96">
      <Navbar
        items={[
          {
            label: "Products",
            columns: [
              {
                label: "Explore",
                featured: true,
                links: [
                  { label: "Analytics", href: "#analytics" },
                  { label: "Automations", href: "#automations" },
                  { label: "Security", href: "#security" },
                ],
              },
              {
                label: "Get started",
                links: [
                  { label: "Pricing", href: "#pricing" },
                  { label: "Customers", href: "#customers" },
                ],
              },
              {
                label: "Learn",
                links: [
                  { label: "Documentation", href: "#docs" },
                  { label: "Help center", href: "#help" },
                ],
              },
            ],
          },
          { label: "Pricing", href: "#pricing" },
          { label: "Blog", href: "#blog" },
        ]}
        brand={brand}
        layout="panel"
      />
    </div>
  )
}
```

## API Reference

### Navbar

The header that renders the brand, navigation items and actions.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `NavbarItem[]` | Required | The navigation items: links, groups of links, or mega items with columns. |
| `brand` | `React.ReactNode` | - | Content shown at the start of the header, such as a logo. |
| `actions` | `React.ReactNode` | - | Content shown at the end of the header, such as a sign-in button; it stays visible in the compact layout. |
| `menuLabel` | `string` | `"Menu"` | Accessible label of the compact menu button and title of the fullscreen menu. |
| `closeLabel` | `string` | - | Text for the fullscreen menu's close button. |
| `layout` | `"popover" \| "panel"` | `"popover"` | With `popover`, dropdowns float beneath their trigger; with `panel`, they open as a full-width panel anchored to the header. |
| `render` | `React.ReactElement` | `<NavbarAnchor />` | The element used for every link, defaulting to a plain anchor; replace it with your router's link component. |

Also accepts the props of `React.ComponentProps<"header">`.

## Accessibility

- Group and mega triggers are buttons with `aria-expanded` reflecting their state, and `Escape` closes an open dropdown.
- Links marked `active` have `aria-current="page"`.
- In the compact layout, the menu button is labelled by `menuLabel`.

## Notes

The header carries `data-slot="navbar"`, `data-layout`, and `data-compact` while the fullscreen menu is in use. The brand and actions wrappers use `data-slot="navbar-brand"` and `data-slot="navbar-actions"`, and mega columns use `data-slot="navbar-column"` with `data-featured`. With `layout="panel"` the header gains `relative` positioning and `z-50` so the panel can anchor to it.

