Navigation Menu

A collection of links for navigating websites.

"use client"

import * as React from "react"
import Link from "next/link"
import { cva } from "class-variance-authority"
import { cn } from "cn"
import { NavigationMenu as NavigationMenuPrimitive } from "@animate-ui/react/primitives/radix-ui"

import { ArrowDown01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

function NavigationMenu({
  className,
  children,
  viewport = true,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
  viewport?: boolean
}) {
  return (
    <NavigationMenuPrimitive.Root
      data-slot="navigation-menu"
      data-viewport={viewport}
      className={cn(
        "max-w-max group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
        className
      )}
      {...props}
    >
      {children}
      {viewport && <NavigationMenuViewport />}
    </NavigationMenuPrimitive.Root>
  )
}

function NavigationMenuList({
  className,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
  return (
    <NavigationMenuPrimitive.List
      data-slot="navigation-menu-list"
      className={cn(
        "gap-0 group flex flex-1 list-none items-center justify-center",
        className
      )}
      {...props}
    />
  )
}

function NavigationMenuItem({
  className,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
  return (
    <NavigationMenuPrimitive.Item
      data-slot="navigation-menu-item"
      className={cn("relative", className)}
      {...props}
    />
  )
}

const navigationMenuTriggerStyle = cva(
  "hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all focus-visible:ring-3 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none"
)

function NavigationMenuTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
  return (
    <NavigationMenuPrimitive.Trigger
      data-slot="navigation-menu-trigger"
      className={cn(navigationMenuTriggerStyle(), "group", className)}
      {...props}
    >
      {children}{" "}
      <HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={2}
        className="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180"
        aria-hidden="true"
      />
    </NavigationMenuPrimitive.Trigger>
  )
}

function NavigationMenuContent({
  className,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
  return (
    <NavigationMenuPrimitive.Content
      data-slot="navigation-menu-content"
      className={cn(
        "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:ring-foreground/10 p-1 group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 top-0 left-0 w-full group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none md:absolute md:w-auto",
        className
      )}
      {...props}
    />
  )
}

function NavigationMenuViewport({
  className,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
  return (
    <div
      className={cn(
        "absolute top-full left-0 isolate z-50 flex justify-center"
      )}
    >
      <NavigationMenuPrimitive.Viewport
        data-slot="navigation-menu-viewport"
        className={cn(
          "bg-popover text-popover-foreground ring-foreground/10 rounded-lg shadow ring-1 origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden md:w-(--radix-navigation-menu-viewport-width)",
          className
        )}
        {...props}
      />
    </div>
  )
}

function NavigationMenuLink({
  className,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
  return (
    <NavigationMenuPrimitive.Link
      data-slot="navigation-menu-link"
      className={cn("data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none focus-visible:ring-3 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md [&_svg:not([class*='size-'])]:size-4", className)}
      {...props}
    />
  )
}

function NavigationMenuIndicator({
  className,
  ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
  return (
    <NavigationMenuPrimitive.Indicator
      data-slot="navigation-menu-indicator"
      className={cn(
        "top-full z-1 flex h-1.5 items-end justify-center overflow-hidden",
        className
      )}
      {...props}
    >
      <div className="bg-border rounded-tl-sm shadow-md relative top-[60%] h-2 w-2 rotate-45" />
    </NavigationMenuPrimitive.Indicator>
  )
}

export {
  NavigationMenu,
  NavigationMenuList,
  NavigationMenuItem,
  NavigationMenuContent,
  NavigationMenuTrigger,
  NavigationMenuLink,
  NavigationMenuIndicator,
  NavigationMenuViewport,
  navigationMenuTriggerStyle,
}

const components: { title: string; href: string; description: string }[] = [
  {
    title: "Alert Dialog",
    href: "/docs/primitives/alert-dialog",
    description:
      "A modal dialog that interrupts the user with important content and expects a response.",
  },
  {
    title: "Hover Card",
    href: "/docs/primitives/hover-card",
    description:
      "For sighted users to preview content available behind a link.",
  },
  {
    title: "Progress",
    href: "/docs/primitives/progress",
    description:
      "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
  },
  {
    title: "Scroll-area",
    href: "/docs/primitives/scroll-area",
    description: "Visually or semantically separates content.",
  },
  {
    title: "Tabs",
    href: "/docs/primitives/tabs",
    description:
      "A set of layered sections of content—known as tab panels—that are displayed one at a time.",
  },
  {
    title: "Tooltip",
    href: "/docs/primitives/tooltip",
    description:
      "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
  },
]

export default function NavigationMenuDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="w-96">
              <ListItem href="/docs" title="Introduction">
                Re-usable components built with Tailwind CSS.
              </ListItem>
              <ListItem href="/docs/installation" title="Installation">
                How to install dependencies and structure your app.
              </ListItem>
              <ListItem href="/docs/primitives/typography" title="Typography">
                Styles for headings, paragraphs, lists...etc
              </ListItem>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem className="hidden md:flex">
          <NavigationMenuTrigger>Components</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-[400px] gap-2 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
              {components.map((component) => (
                <ListItem
                  key={component.title}
                  title={component.title}
                  href={component.href}
                >
                  {component.description}
                </ListItem>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
            <Link href="/docs">Docs</Link>
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

function ListItem({
  title,
  children,
  href,
  ...props
}: React.ComponentPropsWithoutRef<"li"> & { href: string }) {
  return (
    <li {...props}>
      <NavigationMenuLink asChild>
        <Link href={href}>
          <div className="flex flex-col gap-1 text-sm">
            <div className="leading-none font-medium">{title}</div>
            <div className="line-clamp-2 text-muted-foreground">{children}</div>
          </div>
        </Link>
      </NavigationMenuLink>
    </li>
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add navigation-menu
pnpm dlx shadcn@latest add navigation-menu
yarn dlx shadcn@latest add navigation-menu
bun x shadcn@latest add navigation-menu

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate navigation-menu
pnpm dlx @animate-ui/cli@latest animate navigation-menu
yarn dlx @animate-ui/cli@latest animate navigation-menu
bun x @animate-ui/cli@latest animate navigation-menu

Usage

Import the primitive from Animate UI instead of Radix UI. Everything else stays the same.

import { NavigationMenu } from '@animate-ui/react/primitives/radix-ui';

export function SiteNav() {
  return (
    <NavigationMenu.Root>
      <NavigationMenu.List>
        <NavigationMenu.Item>
          <NavigationMenu.Trigger>Learn</NavigationMenu.Trigger>
          <NavigationMenu.Content>
            <NavigationMenu.Link href="/docs">
              Documentation
            </NavigationMenu.Link>
          </NavigationMenu.Content>
        </NavigationMenu.Item>
        <NavigationMenu.Item>
          <NavigationMenu.Link href="/blog">Blog</NavigationMenu.Link>
        </NavigationMenu.Item>
        <NavigationMenu.Indicator />
      </NavigationMenu.List>
      <NavigationMenu.Viewport className="h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width)" />
    </NavigationMenu.Root>
  );
}

Coming from the @radix-ui/react-navigation-menu package? Use the subpath: import * as NavigationMenuPrimitive from '@animate-ui/react/primitives/radix-ui/navigation-menu'.

Animation

Radix's Navigation Menu has no Animate UI v1 counterpart: it opens like v1's Dropdown Menu, and moves between items like Animate UI's Menubar.

  • Viewport fades and scales in from 0.9, dropping from its top edge, in 220ms ({ duration: 0.22, ease: [0.215, 0.61, 0.355, 1] }, a soft ease-out), like v1's menu content, and closes the same way before it unmounts.
  • Moving from one item to another, the Viewport's size follows the new content instead of jumping, on a spring (stiffness 660, damping 52): it starts gently, is most of the way there in ~155ms and eases in without going past, settled by ~300ms, keeping its speed when it changes again before it has landed (never so fast it would go past the next size): Animate UI animates --radix-navigation-menu-viewport-width and --radix-navigation-menu-viewport-height, so classes reading them resize smoothly.
  • Content: moving from one item to another, the content changes in place, like the Menubar's: the previous content leaves 24px away from where the next one comes from (moving to an item on its right, to the left) while it fades out (220ms, a soft ease-out), and the next one comes in from 24px that way on a spring (stiffness 450, damping 43) and fades in behind it (260ms, on a slow start); the Viewport clips them. A copy of the previous content (aria-hidden, inert) leaves in its place. Going back and forth faster than they settle, each content shows once: what's still leaving goes on from where it is, and back to an item whose content is still leaving, it comes back from there (where it is, as it shows) instead of coming in again. Without a Viewport, each Content fades and scales under its item like the Viewport does.
  • Clicking a trigger: a mouse reaches a trigger before it clicks it, and that opens its item, or switches the menu to it. A click within 500ms of that keeps the menu open, like Base UI's "patient click" (Radix toggles it closed): the menu the pointer has just opened doesn't close under the click. Later clicks, the next click, clicks on an item a click opened, triggers that are links (asChild: keeping it open would cancel their navigation), and the keyboard toggle it closed as usual.
  • The highlights: the triggers, and the links beside them (navigationMenuTriggerStyle), share one highlight, like a Menubar's: a trigger hovered, focused or open wears its own background (hover:bg-muted, focus:bg-muted, data-open:bg-muted/50…, read from your styles and cleared on the trigger), and that background glides from trigger to trigger on the highlight's spring (stiffness 500, damping 40). It fades in (200ms, on CSS's ease) when it appears from nothing and fades out where it is the same way when no trigger is lit anymore; lit again meanwhile, it comes back from there. The links of each Content share their own the same way. The List and each Content become their own stacking context (isolation: isolate), so a highlight goes under the text and above the background.
  • Indicator follows the active trigger like v1's menu highlight, on the highlight's spring (stiffness 500, damping 40, quicker than v1's), fading in on it and out 200ms after the menu closes.
  • The highlight's color follows the trigger's state on the same curve: open (data-open:bg-muted/50), then hovered (hover:bg-muted). Without a Viewport, Contents show inside the List, which is its own stacking context for the highlight: a z-index on a Content then orders it among what's inside the List only.
  • Keyboard navigation between links and triggers moves the focus, and the highlight with it wherever your styles light a focused trigger or link (focus:bg-muted).
  • With forceMount, closed parts stay in the DOM: they play their exit, then are hidden (visibility: hidden).
  • An item open from the start (defaultValue) shows with the Viewport's entrance on page load.
  • Under reduced motion, everything only fades and sizes change at once: moving between items, each content fades in place; the highlights jump from trigger to trigger and from link to link.

API Reference

Every part takes exactly the props of Radix UI's Navigation Menu. Animate UI adds the animation props below.

Radix UI props only. Triggers and links take part in the highlights; they need nothing more.

Viewport

Radix UI props, plus:

PropTypeDefault
transition?
Transition{ duration: 0.22, ease: [0.215, 0.61, 0.355, 1] } (the resizing: { type: 'spring', stiffness: 660, damping: 52 })
exitTransition?
Transitiontransition

Content

Radix UI props, plus:

PropTypeDefault
transition?
Transition{ duration: 0.22, ease: [0.215, 0.61, 0.355, 1] }
exitTransition?
Transitiontransition

Indicator

Radix UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 500, damping: 40 }
exitTransition?
Transition{ type: 'spring', stiffness: 500, damping: 40, delay: 0.2 }

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page