NavigationMenu
NavigationLink navigation with keyboard-accessible disclosure panels.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
} from "@/components/ui/navigation-menu";
export default function NavigationMenuExample() {
return (
<div className="min-h-52 w-full">
<NavigationMenu aria-label="Project navigation">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Workspace</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink href="#overview">
Overview
<span className="mt-1 block text-xs text-muted-foreground">
Your projects at a glance
</span>
</NavigationMenuLink>
<NavigationMenuLink href="#activity">
Activity
<span className="mt-1 block text-xs text-muted-foreground">
Recent team updates
</span>
</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink href="#docs">Documentation</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
<p id="overview" className="mt-36 text-xs text-muted-foreground">
Links use local preview anchors.
</p>
<span id="activity" />
<span id="docs" />
</div>
);
}Usage
Compose a list of items containing direct links or a trigger and content panel. Use real destinations in your application; the preview uses local anchors. Link supports asChild for Next.js Link.
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
} from "@/components/ui/navigation-menu";Shipped as editable source in src/components/ui/navigation-menu.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/navigation-menu.tsx
"use client";
import { useDocumentDirection } from "@/lib/use-document-direction";
import type { ComponentProps } from "react";
import { cn } from "@dashboardpack/core/lib/utils";
import { NavigationMenu as Primitive } from "radix-ui";
import { ChevronDown } from "lucide-react";
export function NavigationMenu({
className,
...props
}: ComponentProps<typeof Primitive.Root>) {
const dir = useDocumentDirection();
return (
<Primitive.Root
dir={dir}
className={cn("relative z-10 flex max-w-full items-center", className)}
{...props}
/>
);
}
export function NavigationMenuList({
className,
...props
}: ComponentProps<typeof Primitive.List>) {
return (
<Primitive.List
className={cn("flex flex-wrap items-center gap-1", className)}
{...props}
/>
);
}
export const NavigationMenuItem = Primitive.Item;
export function NavigationMenuTrigger({
className,
children,
...props
}: ComponentProps<typeof Primitive.Trigger>) {
return (
<Primitive.Trigger
className={cn(
"group inline-flex h-9 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-accent disabled:opacity-50",
className,
)}
{...props}
>
{children}
<ChevronDown
className="size-3 transition-transform group-data-[state=open]:rotate-180 motion-reduce:transition-none"
aria-hidden
/>
</Primitive.Trigger>
);
}
export function NavigationMenuContent({
className,
...props
}: ComponentProps<typeof Primitive.Content>) {
return (
<Primitive.Content
className={cn(
"absolute start-0 top-full mt-2 w-72 max-w-[calc(100vw-4rem)] rounded-xl border bg-popover p-2 text-popover-foreground shadow-lg",
className,
)}
{...props}
/>
);
}
export function NavigationMenuLink({
className,
...props
}: ComponentProps<typeof Primitive.Link>) {
return (
<Primitive.Link
className={cn(
"block rounded-md px-3 py-2 text-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring data-[active]:bg-accent",
className,
)}
{...props}
/>
);
}Required helper: src/lib/use-document-direction.ts
Copy this helper alongside the component. It keeps keyboard navigation aligned with the page direction and supports live RTL changes.
"use client";
import { useSyncExternalStore } from "react";
function subscribe(notify: () => void) {
const observer = new MutationObserver(notify);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["dir"],
});
return () => observer.disconnect();
}
function snapshot(): "ltr" | "rtl" {
return document.documentElement.dir === "rtl" ? "rtl" : "ltr";
}
/** Keep headless controls aligned with the document, including live RTL changes. */
export function useDocumentDirection() {
return useSyncExternalStore(subscribe, snapshot, () => "ltr" as const);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| orientation | horizontal | verticalDefault: horizontal | Navigation layout and keyboard orientation. |
| value / defaultValue | stringDefault: — | Controlled or initial open item. |
| onValueChange | (value: string) => voidDefault: — | Respond to disclosure changes. |
| asChild (NavigationMenuLink) | booleanDefault: false | Use your own single link element. |