shadcn.io is not affiliated with official shadcn/ui
Links for the site sit in one row. Some open a panel of related pages, and some are just a link.
Use it for destinations in a header. Commands such as Export or Delete belong in a Dropdown Menu. In Solid, Ark UI renders a navigation landmark, and router links go in the link part so the focus behavior stays.
Installation
Anatomy

NavigationMenu
├── NavigationMenuList
│ ├── NavigationMenuItem
│ │ ├── NavigationMenuTrigger
│ │ └── NavigationMenuContent
│ │ └── NavigationMenuLink
│ └── NavigationMenuItem
│ └── NavigationMenuLink
└── NavigationMenuIndicatorUsage
import {
NavigationMenu, NavigationMenuContent, NavigationMenuItem,
NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Item One</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink href="#">Link</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>Accessibility
- Enter
- Activate the focused control.
- Space
- Activate the focused control.
- Arrow Left
- Move left.
- Arrow Right
- Move right.
- EscEscape
- Close the open panel.
Demos
Rtl
The header in Arabic: a getting-started list, a component grid that wraps into columns, and a docs link with no panel. dir is rtl on the wrapper, so the triggers run from the right and panels follow the reading order. Arrow keys follow that order too. Set direction on the container you ship, and check that a wide panel does not spill off the far edge.
FAQ
How do I use router links in the Solid Navigation Menu?
Render your router's link through NavigationMenuLink, so it keeps the menu's styles and focus handling.
Pass asChild a function that renders your router's A with the props it gets.
How do I highlight the current page in the Navigation Menu?
Set aria-current="page" on the link that matches the current route. Screen readers announce it, and you can style it with Tailwind's aria-[current=page]: variant.
How do I change how fast the Ark UI Navigation Menu opens on hover?
It doesn't open on hover: each panel opens on click, so there's no delay to tune and nothing pops open as the pointer passes over the bar.
How do I control which Navigation Menu panel is open in Solid?
Each NavigationMenuItem is its own popover: pass open and onOpenChange on the item you want to control.
How do I make the Navigation Menu work on mobile?
Hide the bar below md and put the same links in a Sheet behind a menu button. Wide panels under a header bar don't suit small touch screens, and a sheet gives every link a full-width tap target.
When should I use a Navigation Menu instead of a Dropdown Menu or a Menubar?
Use the Navigation Menu for site navigation: links in a header, grouped under triggers. A Dropdown Menu holds actions like Export or Delete, and a Menubar is an app's File, Edit, View bar. Each gives assistive tech the roles that match what it does.
Is the Ark UI Navigation Menu accessible?
Yes. It renders a nav landmark, Tab moves between triggers and links, Enter or Space opens a trigger's panel, and Escape closes it and returns focus to the trigger. Give each link text that makes sense on its own, since screen readers can list them out of context.
API Reference
NavigationMenu
<nav> data-slot="navigation-menu"
class?string
Merged with the part's classes.
NavigationMenuItem
<li>
open?boolean
The controlled open state of the popover.
defaultOpen?boolean
The initial open state of the popover when rendered. Use when you don't need to control the open state of the popover.
onOpenChange?(details: OpenChangeDetails) → void
Function invoked when the popover opens or closes.
- Type
(details: OpenChangeDetails) → void- Parameters
details– { open: boolean }- Returns
void
triggerValue?string | null
The controlled trigger value.
- Type
string | null
defaultTriggerValue?string | null
The initial trigger value when rendered. Use when you don't need to control the trigger value.
- Type
string | null
onTriggerValueChange?(details: TriggerValueChangeDetails) → void
Function called when the trigger value changes.
- Type
(details: TriggerValueChangeDetails) → void- Parameters
details– { value: string | null; triggerElement: HTMLElement | null }- Returns
void
autoFocus?boolean
Whether to automatically set focus on the first focusable content within the popover when opened.
- Default
true
closeOnEscape?boolean
Whether to close the popover when the escape key is pressed.
- Default
true
closeOnInteractOutside?boolean
Whether to close the popover when the user clicks outside of the popover.
- Default
true
dir?"ltr" | "rtl"
The document's text/writing direction.
- Type
"ltr" | "rtl"- Default
"ltr"
finalFocusEl?() → MaybeElement
Element to receive focus when the popover is closed.
- Returns
MaybeElement
hideMode?"display-none" | "activity"
How to hide content when mounted but not present.
- display-none: HTML hidden attribute. Effects stay alive.
- activity: React 19 <Activity mode="hidden">. Effects pause. Requires React 19+.
- Type
"display-none" | "activity"- Default
"display-none"
id?string
The unique identifier of the machine.
ids?object
The ids of the elements in the popover. Useful for composition.
- Type
{ anchor?: string; trigger?: string | ((value?: string) → string); content?: string; title?: string; description?: string; closeTrigger?: string; positioner?: string; arrow?: string }
initialFocusEl?() → HTMLElement | null | false
The element to focus on when the popover is opened.
Return false to skip moving focus, or null to use the default.
- Type
() → HTMLElement | null | false- Returns
HTMLElement | null | false
lazyMount?boolean
Whether to enable lazy mounting. Ark default: false.
- Default
true
modal?boolean
Whether the popover should be modal. When set to true:
- interaction with outside elements will be disabled
- only popover content will be visible to screen readers
- scrolling is blocked
- focus is trapped within the popover
- Default
false
onEscapeKeyDown?(event: KeyboardEvent) → void
Function called when the escape key is pressed.
- Parameters
event– { altKey: boolean; charCode: number; ctrlKey: boolean; code: string; getModifierState: (key: string) → boolean; key: string; keyCode: number; locale: string; location: number; metaKey: boolean; repeat: boolean; shiftKey: boolean; which: number }- Returns
void
onFocusOutside?(event: FocusOutsideEvent) → void
Function called when the focus is moved outside the component.
- Type
(event: FocusOutsideEvent) → void- Parameters
event– FocusOutsideEvent- Returns
void
onInteractOutside?(event: InteractOutsideEvent) → void
Function called when an interaction happens outside the component.
- Type
(event: InteractOutsideEvent) → void- Parameters
event– InteractOutsideEvent- Returns
void
onPointerDownOutside?(event: PointerDownOutsideEvent) → void
Function called when the pointer is pressed down outside the component.
- Type
(event: PointerDownOutsideEvent) → void- Parameters
event– PointerDownOutsideEvent- Returns
void
onRequestDismiss?(event: LayerDismissEvent) → void
Function called when this layer is closed due to a parent layer being closed.
- Type
(event: LayerDismissEvent) → void- Parameters
event– LayerDismissEvent- Returns
void
persistentElements?Array<() → Element | null>
Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event
- Type
Array<() → Element | null>
portalled?boolean
Whether the popover is portalled. This will proxy the tabbing behavior regardless of the DOM position of the popover content.
- Default
true
positioning?object
The user provided options used to position the popover content.
- Type
{ restoreStyles?: boolean; applyStyles?: boolean; hideWhenDetached?: boolean; strategy?: "absolute" | "fixed"; placement?: Placement; offset?: { mainAxis?: number; crossAxis?: number; }; gutter?: number; shift?: number; overflowPadding?: number; arrowPadding?: number; flip?: boolean | Placement[]; slide?: boolean; overlap?: boolean; sameWidth?: boolean; fitViewport?: boolean; sizeMiddleware?: boolean; boundary?: (() → Boundary) | Boundary | "clipping-ancestors"; listeners?: boolean | AutoUpdateOptions; onComplete?: (data: ComputePositionReturn) → void; onPositioned?: ((data: { placed: boolean; }) → void); getAnchorElement?: () → HTMLElement | VirtualElement | null; getAnchorRect?: (element: HTMLElement | VirtualElement | null) → AnchorRect | null; updatePosition?: ((data: { updatePosition: () → Promise<void>; floatingElement: HTMLElement | null; }) → void | Promise<void>) }- Default
{ gutter: 6, placement: "bottom-start" }
restoreFocus?boolean
Whether to restore focus to the element that had focus before the popover was opened.
- Default
true
skipAnimationOnMount?boolean
Whether to allow the initial presence animation.
- Default
false
translations?object
Specifies the localized strings that identifies the accessibility elements and their states.
- Type
{ closeTriggerLabel?: string }
unmountOnExit?boolean
Whether to unmount on exit. Ark default: false.
- Default
true
asChild?(props: ParentProps) => JSX.Element
Render your own element: a function that gets the part's props to spread on it.
- Type
(props: ParentProps) => JSX.Element
class?string
Merged with the part's classes.
NavigationMenuTrigger
<button> data-slot="navigation-menu-trigger"
value?string
The value that identifies this specific trigger.
asChild?(props: ParentProps) => JSX.Element
Render your own element: a function that gets the part's props to spread on it.
- Type
(props: ParentProps) => JSX.Element
class?string
Merged with the part's classes.
data-current?string
Present when current.
data-placement?string
The placement of the trigger.
data-side?string
The side of the trigger that the trigger is positioned on.
data-state"open" | "closed"
"open" or "closed".
- Type
"open" | "closed"
data-value?string
The value of the item.
NavigationMenuPositioner
<div> data-slot="navigation-menu-positioner"
asChild?(props: ParentProps) => JSX.Element
Render your own element: a function that gets the part's props to spread on it.
- Type
(props: ParentProps) => JSX.Element
class?string
Merged with the part's classes.
data-has-nested?string
Present when the positioner has nested popovers.
data-nested?string
Present when the positioner is nested within another popover.
--available-heightstring
The available height in viewport.
--available-widthstring
The available width in viewport.
--reference-heightstring
The height of the root.
--reference-widthstring
The width of the reference element.
--transform-originstring
The transform origin for animations.
--xstring
The x position for transform.
--ystring
The y position for transform.
--z-indexstring
The z-index value.
NavigationMenuContent
<div> data-slot="navigation-menu-content"
asChild?(props: ParentProps) => JSX.Element
Render your own element: a function that gets the part's props to spread on it.
- Type
(props: ParentProps) => JSX.Element
class?string
Merged with the part's classes.
data-expanded?string
Present when expanded.
data-has-nested?string
Present when another popover is nested inside it.
data-nested?string
Present when it's nested inside another popover.
data-placementstring
The placement of the content.
data-side"top" | "right" | "bottom" | "left"
The side of the trigger that the content is positioned on.
- Type
"top" | "right" | "bottom" | "left"
data-state"open" | "closed"
"open" or "closed".
- Type
"open" | "closed"
--layer-indexstring
The index of the dismissable in the layer stack.
--nested-layer-countstring
The number of nested popovers.
--transform-originstring
Where the content scales from, on its trigger's side.
NavigationMenuList
<ul> data-slot="navigation-menu-list"
class?string
Merged with the part's classes.
NavigationMenuLink
<a> data-slot="navigation-menu-link"
asChild?(props: ParentProps) => JSX.Element
Render your own element: a function that gets the part's props to spread on it.
- Type
(props: ParentProps) => JSX.Element
class?string
Merged with the part's classes.
NavigationMenuIndicator
<div> data-slot="navigation-menu-indicator"
class?string
Merged with the part's classes.
NavigationMenuViewport
<div> data-slot="navigation-menu-viewport"
class?string
Merged with the part's classes.
With other components
Button
ButtonsTrigger looks like a button
Sheet
OverlaysSame links on phones
Badge
FeedbackNew next to a link
Card
LayoutFeatured cell in mega menu
Separator
LayoutSplit groups in a panel
Breadcrumb
NavigationTrail under the header
Related
Dropdown Menu
MenusActions, not site nav
Menubar
MenusFile, Edit, View
Sidebar
NavigationPersistent app chrome
Hover Card
OverlaysPreview behind a link
Tabs
NavigationSame-page sections
Bottom Navigation
NavigationFixed to the bottom edge