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.