Navigation Menu
Accessible navigation menu with links and sub-navigation. Install Navigation Menu from the VLLNT UI registry with the shadcn CLI.
Accessible navigation menu with links and sub-navigation. Part of the navigation family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Navigation Menu to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/navigation-menu.jsonSource
"use client";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "../../lib/utils";
const NavigationMenu = ({
children,
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> & {
ref?: React.Ref<React.ComponentRef<typeof NavigationMenuPrimitive.Root>>;
}) => (
<NavigationMenuPrimitive.Root
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
ref={ref}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
);
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = ({
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> & {
ref?: React.Ref<React.ComponentRef<typeof NavigationMenuPrimitive.List>>;
}) => (
<NavigationMenuPrimitive.List
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
ref={ref}
{...props}
/>
);
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = ({
children,
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> & {
ref?: React.Ref<React.ComponentRef<typeof NavigationMenuPrimitive.Trigger>>;
}) => (
<NavigationMenuPrimitive.Trigger
className={cn(navigationMenuTriggerStyle(), "group", className)}
ref={ref}
{...props}
>
{children}{" "}
<ChevronDown
aria-hidden="true"
className="relative top-[1px] ml-1 size-3 transition duration-200 group-data-[state=open]:rotate-180"
/>
</NavigationMenuPrimitive.Trigger>
);
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = ({
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> & {
ref?: React.Ref<React.ComponentRef<typeof NavigationMenuPrimitive.Content>>;
}) => (
<NavigationMenuPrimitive.Content
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
ref={ref}
{...props}
/>
);
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = ({
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> & {
ref?: React.Ref<React.ComponentRef<typeof NavigationMenuPrimitive.Viewport>>;
}) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
);
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = ({
className,
ref,
...props
}: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> & {
ref?: React.Ref<React.ComponentRef<typeof NavigationMenuPrimitive.Indicator>>;
}) => (
<NavigationMenuPrimitive.Indicator
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
ref={ref}
{...props}
>
<div className="relative top-[60%] size-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
NavigationMenu,
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
NavigationMenuViewport,
};
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.