Header

Responsive header with navigation and theme toggle.


Installation

CLI

npx shadcn@latest add "https://ui.xaclabs.dev/r/header.json"

Usage

import { Header } from "@/registry/components/header"
View component source
header.tsx
tsx
"use client"

import type { LucideIcon } from "lucide-react"
import { Menu as MenuIcon } from "lucide-react"
import { motion } from "motion/react"
import { Button } from "@/components/ui/button"
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { cn, resolveLink } from "@/lib/utils"
import { Container } from "@/registry/components/container"
import { ThemeSwitcher } from "@/registry/components/theme-switcher"

export interface Menu {
  title: string
  href?: string
  icon?: LucideIcon
  children?: Menu[]
}

export interface HeaderProps {
  header: {
    title: string
    description: string
    menus: Menu[]
  }
  variant?: "contained" | "full"
}

function MobileNav({ menus }: { menus: Menu[] }) {
  return (
    <nav className="flex flex-col gap-4 p-4">
      {menus.map((menu) => {
        const { title, href, children } = menu
        if (children) {
          return (
            <div key={title} className="space-y-2">
              <span className="text-sm font-medium text-muted-foreground">
                {title}
              </span>
              <div className="flex flex-col gap-1 pl-2">
                {children.map(({ title, href }) => (
                  <a
                    key={title}
                    href={resolveLink(href || "")}
                    className="text-sm py-1 hover:text-muted-foreground transition-colors"
                  >
                    {title}
                  </a>
                ))}
              </div>
            </div>
          )
        }
        return (
          <a
            key={title}
            href={resolveLink(href || "")}
            className="text-sm font-medium py-1 hover:text-muted-foreground transition-colors"
          >
            {title}
          </a>
        )
      })}
    </nav>
  )
}

export function Header({ header, variant = "contained" }: HeaderProps) {
  const { title, menus } = header

  const inner = (
    <div
      className={cn(
        "flex items-center justify-between py-4 border-b",
        variant === "full" ? "px-6 md:px-8" : "",
      )}
    >
      <div className="flex items-center gap-2">
        <a
          href="/"
          className="text-sm font-bold hover:text-muted-foreground transition-colors"
        >
          {title}
        </a>
      </div>

      {/* Desktop navigation */}
      <div className="hidden md:block">
        <NavigationMenu className="mx-auto">
          <NavigationMenuList>
            {menus.map((menu) => {
              const { title, href, children } = menu
              return (
                <NavigationMenuItem key={title}>
                  {children ? (
                    <>
                      <NavigationMenuTrigger>{title}</NavigationMenuTrigger>
                      <NavigationMenuContent>
                        <ul className="grid gap-3 p-4">
                          {children.map(({ title, href }) => (
                            <li key={title}>
                              <a
                                href={resolveLink(href || "")}
                                className="hover:text-muted-foreground transition-colors"
                              >
                                {title}
                              </a>
                            </li>
                          ))}
                        </ul>
                      </NavigationMenuContent>
                    </>
                  ) : (
                    <NavigationMenuLink
                      href={href}
                      className={cn(navigationMenuTriggerStyle())}
                    >
                      {title}
                    </NavigationMenuLink>
                  )}
                </NavigationMenuItem>
              )
            })}
          </NavigationMenuList>
        </NavigationMenu>
      </div>

      <div className="flex items-center gap-2">
        <ThemeSwitcher />

        {/* Mobile hamburger */}
        <Sheet>
          <SheetTrigger asChild>
            <Button variant="ghost" size="icon" className="md:hidden">
              <MenuIcon className="h-5 w-5" />
              <span className="sr-only">Open menu</span>
            </Button>
          </SheetTrigger>
          <SheetContent side="right" className="w-72">
            <MobileNav menus={menus} />
          </SheetContent>
        </Sheet>
      </div>
    </div>
  )

  return (
    <motion.div
      initial={{ opacity: 0, y: -10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      {variant === "contained" ? <Container>{inner}</Container> : inner}
    </motion.div>
  )
}

Examples

Full Width

Full-width header variant without container wrapping.