HextaUI

Navigation menu

Site navigation with dropdowns, or one full-width panel that stays open while its content slides between menus.

import {
  IconBook,
  IconBrandGithub,
  IconBuildingStore,
  IconChartBar,
  IconCode,
  IconComponents,
  IconLayoutDashboard,
  IconLifebuoy,
  IconPalette,
  IconRocket,
  IconShieldLock,
  IconUsers,
} from "@tabler/icons-react"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"

type Entry = {
  title: string
  description: string
  icon: typeof IconBook
}

const products: Entry[] = [
  {
    title: "Components",
    description: "Accessible building blocks with motion built in.",
    icon: IconComponents,
  },
  {
    title: "Blocks",
    description: "Full sections you can drop into a page.",
    icon: IconLayoutDashboard,
  },
  {
    title: "Themes",
    description: "Tokens for color, radius and type.",
    icon: IconPalette,
  },
  {
    title: "Templates",
    description: "Starter apps ready to deploy.",
    icon: IconBuildingStore,
  },
]

const solutions: Entry[] = [
  {
    title: "Startups",
    description: "Ship a polished product on day one.",
    icon: IconRocket,
  },
  {
    title: "Teams",
    description: "One system shared across every app.",
    icon: IconUsers,
  },
  {
    title: "Analytics",
    description: "Dashboards, tables and charts.",
    icon: IconChartBar,
  },
  {
    title: "Security",
    description: "Audited for accessibility and safe defaults.",
    icon: IconShieldLock,
  },
  {
    title: "Developers",
    description: "Typed APIs that read like shadcn.",
    icon: IconCode,
  },
  {
    title: "Open source",
    description: "Read every line on GitHub.",
    icon: IconBrandGithub,
  },
]

const resources: Entry[] = [
  {
    title: "Documentation",
    description: "Every component, example and prop.",
    icon: IconBook,
  },
  {
    title: "Support",
    description: "Questions, bugs and feature requests.",
    icon: IconLifebuoy,
  },
]

function Links({ entries }: { entries: Entry[] }) {
  return (
    <ul className="grid gap-1 sm:grid-cols-2">
      {entries.map((entry) => (
        <li key={entry.title}>
          <NavigationMenuLink href="#">
            <span className="flex size-9 shrink-0 items-center justify-center rounded-md bg-background ring-(length:--hairline) ring-border">
              <entry.icon className="text-foreground" />
            </span>
            <span className="flex min-w-0 flex-col">
              <span className="font-medium">{entry.title}</span>
              <span className="text-muted-foreground">{entry.description}</span>
            </span>
          </NavigationMenuLink>
        </li>
      ))}
    </ul>
  )
}

export function NavigationMenuDemo() {
  return (
    <div className="w-full max-w-2xl rounded-xl border px-2 py-1.5">
      <NavigationMenu variant="panel" aria-label="Main">
        <NavigationMenuList>
          <NavigationMenuItem>
            <NavigationMenuTrigger>Products</NavigationMenuTrigger>
            <NavigationMenuContent>
              <Links entries={products} />
            </NavigationMenuContent>
          </NavigationMenuItem>
          <NavigationMenuItem>
            <NavigationMenuTrigger>Solutions</NavigationMenuTrigger>
            <NavigationMenuContent>
              <Links entries={solutions} />
            </NavigationMenuContent>
          </NavigationMenuItem>
          <NavigationMenuItem>
            <NavigationMenuTrigger>Resources</NavigationMenuTrigger>
            <NavigationMenuContent>
              <Links entries={resources} />
            </NavigationMenuContent>
          </NavigationMenuItem>
          <NavigationMenuItem>
            <NavigationMenuLink href="#">Pricing</NavigationMenuLink>
          </NavigationMenuItem>
        </NavigationMenuList>
      </NavigationMenu>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/navigation-menu.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"
<NavigationMenu variant="panel" aria-label="Main">
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Products</NavigationMenuTrigger>
      <NavigationMenuContent>
        <NavigationMenuLink href="/components">Components</NavigationMenuLink>
      </NavigationMenuContent>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink href="/pricing">Pricing</NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>

With variant="panel", one card spans the whole bar. Moving between triggers keeps it open and in place: only the content slides toward the direction you moved, and the height eases to fit. Nothing closes, reopens or jumps.

NavigationMenu
└── NavigationMenuList
    ├── NavigationMenuItem
    │   ├── NavigationMenuTrigger
    │   └── NavigationMenuContent
    │       └── NavigationMenuLink
    └── NavigationMenuItem
        └── NavigationMenuLink

The default dropdown variant opens a card under each trigger. Moving to the next trigger glides the same card over and resizes it to the new content.

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"

const guides = [
  { title: "Installation", description: "Add HextaUI to a Next.js app." },
  { title: "Theming", description: "Colors, radius and dark mode." },
  { title: "Motion", description: "Durations, easing and reduced motion." },
]

const components = [
  "Accordion",
  "Button",
  "Dialog",
  "Menubar",
  "Table",
  "Tooltip",
]

export function NavigationMenuDropdown() {
  return (
    <NavigationMenu aria-label="Docs">
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Guides</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="flex w-72 flex-col gap-1">
              {guides.map((guide) => (
                <li key={guide.title}>
                  <NavigationMenuLink href="#">
                    <span className="flex flex-col">
                      <span className="font-medium">{guide.title}</span>
                      <span className="text-muted-foreground">
                        {guide.description}
                      </span>
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Components</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-80 grid-cols-2 gap-1">
              {components.map((name) => (
                <li key={name}>
                  <NavigationMenuLink href="#">{name}</NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink href="#">Blog</NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Current page

Links in the bar look like triggers. Mark the current page with active, which also sets aria-current="page".

import {
  NavigationMenu,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
} from "@/components/ui/navigation-menu"

const links = ["Overview", "Activity", "Settings"]

export function NavigationMenuActive() {
  return (
    <NavigationMenu aria-label="Project">
      <NavigationMenuList>
        {links.map((link) => (
          <NavigationMenuItem key={link}>
            <NavigationMenuLink href="#" active={link === "Activity"}>
              {link}
            </NavigationMenuLink>
          </NavigationMenuItem>
        ))}
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Right to left

Content slides follow the direction you move, which flips in right-to-left layouts.

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"

const menus = [
  { label: "المنتجات", links: ["المكونات", "القوالب", "السمات"] },
  { label: "الموارد", links: ["التوثيق", "الدعم"] },
]

export function NavigationMenuRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md rounded-xl border px-2 py-1.5">
      <NavigationMenu variant="panel" aria-label="الرئيسية">
        <NavigationMenuList>
          {menus.map((menu) => (
            <NavigationMenuItem key={menu.label}>
              <NavigationMenuTrigger>{menu.label}</NavigationMenuTrigger>
              <NavigationMenuContent>
                <ul className="grid grid-cols-2 gap-1">
                  {menu.links.map((link) => (
                    <li key={link}>
                      <NavigationMenuLink href="#">{link}</NavigationMenuLink>
                    </li>
                  ))}
                </ul>
              </NavigationMenuContent>
            </NavigationMenuItem>
          ))}
        </NavigationMenuList>
      </NavigationMenu>
    </div>
  )
}
KeyAction
TabMoves through the bar, and from an open trigger into its content.
EnterSpaceOpens or closes the focused trigger's menu.
↑↓Moves between links in the open content.
←→Moves between triggers in the bar.
EscCloses the menu and returns focus to its trigger.
  • The root renders a <nav>. Give it an aria-label when the page has more than one.
  • Triggers are buttons that report whether their menu is open, and focus returns to them when the menu closes.
  • The content slide and the sliding highlight turn into fades when reduced motion is on.
PropTypeDefault
variantdropdown opens under each trigger. panel opens one full-width card under the bar that stays put while content slides.
"dropdown" | "panel""dropdown"
showSafeAreaDraws the safe area that keeps a menu open while the pointer heads into it. For debugging and demos.
booleanfalse
align
"start" | "center" | "end""start", or "center" for panel
sideOffset
number8
value
any–
defaultValue
any–
onValueChange
(value, details) => void–
delayWait before opening on hover, in ms.
number50
closeDelayWait before closing after the pointer leaves, in ms.
number50
orientation
"horizontal" | "vertical""horizontal"
AttributeDescription
data-slot="navigation-menu"The nav, with data-variant.
data-slot="navigation-menu-highlight"The background that slides between open triggers.
data-slot="navigation-menu-popup"The card. Its height eases through --popup-height.
AttributeDescription
data-slot="navigation-menu-trigger"Target triggers in CSS.
data-popup-openPresent while its menu is open. The chevron flips.
AttributeDescription
data-slot="navigation-menu-content"A menu's content, with data-variant.
data-activation-direction"left", "right", "up" or "down": where the previous trigger was. Drives the slide.
--navigation-menu-link-radiusLink corner radius, concentric with the card.
PropTypeDefault
activeMarks the current page.
booleanfalse
render
ReactElement | (props, state) => ReactElement<a>
AttributeDescription
data-slot="navigation-menu-link"Target links in CSS. In the bar they size themselves like triggers.
data-activePresent on the current page.