Skip to main content

NavigationMenu Component

A collection of accessible, customizable links for navigating websites with nested content support and smooth animations.

Import

import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
NavigationMenuViewport,
NavigationMenuIndicator,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';

Usage

Basic Usage

<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Item One</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink>Link</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
import Link from 'next/link';

<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<Link href="/docs" legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
Documentation
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>

With Content Grid

<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Getting Started</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-2">
<li>
<NavigationMenuLink asChild>
<a href="/introduction" className="block p-2 rounded-md hover:bg-accent">
Introduction
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<a href="/installation" className="block p-2 rounded-md hover:bg-accent">
Installation
</a>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>

Component Structure

  • NavigationMenu: The root component that wraps the entire navigation menu structure
  • NavigationMenuList: Contains the list of navigation items
  • NavigationMenuItem: An individual navigation item
  • NavigationMenuTrigger: Trigger button for dropdown content
  • NavigationMenuContent: Content that appears when trigger is activated
  • NavigationMenuLink: Navigation link that can be used directly or with asChild
  • NavigationMenuViewport: Viewport container for displaying menu content
  • NavigationMenuIndicator: Visual indicator for the active menu item

Props

PropTypeDefaultDescription
valuestringundefinedThe controlled value of the menu item to activate
defaultValuestringundefinedThe value of the menu item that should be active when initially rendered
onValueChange(value: string) => voidundefinedEvent handler called when the value changes
delayDurationnumber200The duration from when a menu item is hovered until the content is shown
skipDelayDurationnumber300The duration from when an item is no longer hovered until the content begins hiding
dir`"ltr""rtl"`"ltr"
orientation`"horizontal""vertical"`"horizontal"
PropTypeDefaultDescription
forceMountbooleanfalseForce mounting of the content when true
classNamestringundefinedAdditional CSS classes to apply
PropTypeDefaultDescription
asChildbooleanfalseWhen true, component will render as its children instead of a default element
activebooleanfalseWhether the link is currently active
classNamestringundefinedAdditional CSS classes to apply
PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes to apply
childrenReactNodeRequiredContent for the trigger button

TypeScript

// Component interface from Radix UI
interface NavigationMenuProps extends React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
delayDuration?: number;
skipDelayDuration?: number;
dir?: "ltr" | "rtl";
orientation?: "horizontal" | "vertical";
}

// Link props
interface NavigationMenuLinkProps extends React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Link> {
asChild?: boolean;
active?: boolean;
}

// Content props
interface NavigationMenuContentProps extends React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> {
forceMount?: boolean;
}

Customization

Style Overrides

The component can be customized using the following approaches:

  1. Using the className prop to add additional Tailwind classes:
<NavigationMenu className="bg-slate-100 dark:bg-slate-900 rounded-md">
{/* Navigation menu content */}
</NavigationMenu>
  1. Using the navigationMenuTriggerStyle function for custom trigger styles:
import { navigationMenuTriggerStyle } from '@/components/ui/navigation-menu';

<NavigationMenuLink
className={cn(
navigationMenuTriggerStyle(),
"font-bold text-green-600 dark:text-green-400"
)}
>
Custom Styled Link
</NavigationMenuLink>
  1. Applying styles to child elements using CSS selector patterns:
<NavigationMenuContent className="p-4 [&>ul]:grid [&>ul]:grid-cols-2 [&>ul]:gap-4">
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</NavigationMenuContent>

Creating a Custom Navigation List Item

import {
NavigationMenuLink,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
import { cn } from '@/lib/utils';

const ListItem = React.forwardRef<
React.ElementRef<'a'>,
React.ComponentPropsWithoutRef<'a'>
>(({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
ref={ref}
className={cn(
'block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
className
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
);
});
ListItem.displayName = 'ListItem';

Examples

Integration with Header Component

import { 
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
navigationMenuTriggerStyle
} from '@/components/ui/navigation-menu';
import Link from 'next/link';

export function Header() {
return (
<header className="w-full py-4 px-6 border-b border-gray-200">
<div className="container mx-auto flex items-center justify-between">
<Link href="/" className="font-bold text-xl">
Your Logo
</Link>

<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Products</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2">
<li>
<NavigationMenuLink asChild>
<Link href="/products/software" className="block p-3 rounded-md hover:bg-accent">
<div className="font-medium mb-1">Software</div>
<p className="text-sm text-muted-foreground">Our software solutions</p>
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link href="/products/hardware" className="block p-3 rounded-md hover:bg-accent">
<div className="font-medium mb-1">Hardware</div>
<p className="text-sm text-muted-foreground">Our hardware products</p>
</Link>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>

<NavigationMenuItem>
<Link href="/about" legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
About
</NavigationMenuLink>
</Link>
</NavigationMenuItem>

<NavigationMenuItem>
<Link href="/contact" legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
Contact
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
</header>
);
}

Mobile-Responsive Navigation Menu

'use client';

import { useState } from 'react';
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
navigationMenuTriggerStyle
} from '@/components/ui/navigation-menu';
import { Button } from '@/components/ui/button';
import { Menu, X } from 'lucide-react';
import Link from 'next/link';
import { cn } from '@/lib/utils';

export function ResponsiveNavigation() {
const [isOpen, setIsOpen] = useState(false);

return (
<nav className="w-full relative">
{/* Mobile menu button */}
<div className="md:hidden flex justify-end p-4">
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(!isOpen)}
aria-label={isOpen ? "Close menu" : "Open menu"}
>
{isOpen ? <X /> : <Menu />}
</Button>
</div>

{/* Navigation for desktop and mobile */}
<div className={cn(
"md:block",
isOpen ? "block" : "hidden"
)}>
<NavigationMenu className="w-full max-w-full justify-start md:justify-center">
<NavigationMenuList className="flex-col md:flex-row">
<NavigationMenuItem>
<NavigationMenuTrigger>Features</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[200px] md:w-[400px] gap-3 p-4 md:grid-cols-2">
<li>
<NavigationMenuLink asChild>
<a href="#feature-1" className="block p-2 rounded-md hover:bg-accent">
Feature 1
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<a href="#feature-2" className="block p-2 rounded-md hover:bg-accent">
Feature 2
</a>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>

<NavigationMenuItem>
<Link href="/docs" legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
Documentation
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
</nav>
);
}

Responsive Behavior

The component responds to different screen sizes in the following ways:

  • Mobile:

    • By default, the component is horizontally oriented and can overflow on small screens
    • Customizations are needed (as shown in example above) to make it fully responsive
    • Content appears as a dropdown below the trigger
  • Tablet and Desktop:

    • Full horizontal layout with space between items
    • Content has more sophisticated positioning with md:w-auto and md:absolute classes
    • Viewport has improved animations and styling with shadow effects

Accessibility

The NavigationMenu component follows these accessibility best practices:

  • Adheres to the navigation role requirements from WAI-ARIA
  • Provides keyboard navigation with Tab, Enter, Space, Arrow keys
  • Supports screen readers with proper ARIA attributes
  • Uses aria-hidden="true" for decorative icons
  • Includes proper focus management and visible focus styles
  • Content elements have proper ARIA roles and attributes from Radix UI

Implementation Details

The component:

  • Is built on top of Radix UI's Navigation Menu primitive
  • Uses CSS variables for animation effects (via data-[state] and data-[motion] attributes)
  • Implements responsive styles using Tailwind's mobile-first approach
  • Supports a viewport system for displaying content in a consistent, accessible way
  • Uses animated transitions for enhanced user experience

Common Pitfalls

  • Missing use client directive: If you're creating a custom component with NavigationMenu, remember to add the 'use client' directive at the top of your file when using Next.js App Router.

  • Incorrect usage with Next.js Links: When using with Next.js Link component, make sure to use legacyBehavior and passHref props to properly pass the href to the NavigationMenuLink.

  • Nested Navigation Menus: Avoid nesting NavigationMenu components directly. Instead, create a single top-level NavigationMenu and use composition for complex navigation structures.

  • Styling inconsistencies: When applying custom styles, remember that some properties come from Radix UI primitives and might need specific selectors or data attributes to override.

  • Missing Viewport component: The NavigationMenuViewport is crucial for proper rendering of content. It's included by default in the NavigationMenu component, but if you're customizing, make sure it's included.

Testing

// Example test for the NavigationMenu component
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
} from '@/components/ui/navigation-menu';

describe('NavigationMenu', () => {
it('renders correctly', () => {
render(
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Test Trigger</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink>Test Link</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);

expect(screen.getByText('Test Trigger')).toBeInTheDocument();
});

it('shows content when trigger is clicked', async () => {
render(
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Test Trigger</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink>Test Link</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);

const trigger = screen.getByText('Test Trigger');
await userEvent.click(trigger);

// Content should now be visible
expect(screen.getByText('Test Link')).toBeVisible();
});
});
  • DropdownMenu: Similar in functionality but designed for application commands rather than navigation links
  • Tabs: Another way to organize content into sections, but with a different interaction model