Navigation
Navigation Menu.
A header menu whose triggers open panels of links in a shared viewport, with an orange indicator under the open trigger.
"use client";
import * as React from "react";
import {
BotIcon,
BoxIcon,
CompassIcon,
FuelIcon,
GlobeIcon,
PickaxeIcon,
RadioTowerIcon,
StoreIcon,
} from "lucide-react";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
const hubs = [
{ title: "Directory", href: "#", icon: GlobeIcon, description: "Every hub site on the server." },
{ title: "East Market", href: "#", icon: StoreIcon, description: "Shops, prices and trade posts." },
{ title: "Rednet relay", href: "#", icon: RadioTowerIcon, description: "Network status and routes." },
{ title: "Storage", href: "#", icon: BoxIcon, description: "Chest indexes for the vault." },
];
const turtles = [
{ title: "Miners", href: "#", icon: PickaxeIcon, description: "Quarry and tunnel crews." },
{ title: "Couriers", href: "#", icon: CompassIcon, description: "Item runs between hubs." },
{ title: "Fuel depot", href: "#", icon: FuelIcon, description: "Coal stock and refuel stops." },
{ title: "All turtles", href: "#", icon: BotIcon, description: "Status for the whole fleet." },
];
function PanelLink({
title,
href,
icon: Icon,
description,
}: {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
description: string;
}) {
return (
<li>
<NavigationMenuLink asChild>
<a href={href} className="flex-row items-start gap-3">
<span className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-md border bg-accent">
<Icon className="size-3.5 text-[#b7ca9e]" />
</span>
<span className="grid gap-0.5">
<span className="font-medium text-foreground">{title}</span>
<span className="line-clamp-2 text-xs leading-snug text-muted-foreground">{description}</span>
</span>
</a>
</NavigationMenuLink>
</li>
);
}
export default function NavigationMenuDemo() {
return (
<div className="flex min-h-[360px] w-full items-start justify-center">
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Hubs</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid w-full gap-2 md:w-[540px] md:grid-cols-[180px_1fr]">
<NavigationMenuLink asChild>
<a
href="#"
className="hidden justify-end gap-1.5 border bg-accent bg-grid p-4 hover:border-primary/60 hover:bg-accent md:flex"
>
<span className="font-mono text-[10px] tracking-[0.2em] text-success uppercase">Featured</span>
<span className="font-display text-lg leading-tight font-medium tracking-tight text-foreground">
quarry.hub
</span>
<span className="text-xs leading-snug text-muted-foreground">
Live depth charts from turtle 07 and friends.
</span>
</a>
</NavigationMenuLink>
<ul className="grid gap-1">
{hubs.map((hub) => (
<PanelLink key={hub.title} {...hub} />
))}
</ul>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Turtles</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-full gap-1 md:w-[480px] md:grid-cols-2">
{turtles.map((turtle) => (
<PanelLink key={turtle.title} {...turtle} />
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<a href="#">Guestbook</a>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuIndicator />
</NavigationMenuList>
</NavigationMenu>
</div>
);
}Installation#
With the CLI
$npx lanterncn add navigation-menuThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/navigation-menu. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install radix-ui class-variance-authority lucide-reactCopy the source into your project
components/ui/navigation-menu.tsx"use client"; import * as React from "react"; import { cva } from "class-variance-authority"; import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui"; import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; 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("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("group flex flex-1 list-none items-center justify-center gap-0.5", className)} {...props} /> ); } function NavigationMenuItem({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) { return ( <NavigationMenuPrimitive.Item data-slot="navigation-menu-item" className={cn("group-data-[viewport=false]/navigation-menu:relative", className)} {...props} /> ); } const navigationMenuTriggerStyle = cva( "group inline-flex h-9 w-max cursor-pointer items-center justify-center gap-1 rounded-md px-3 text-[13px] font-medium text-muted-foreground transition-colors outline-none select-none hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-40 data-[state=open]:text-foreground data-[active=true]:text-primary sm:px-3.5", ); 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} <ChevronDownIcon className="relative top-px size-3 text-muted-foreground transition duration-300 group-data-[state=open]:rotate-180 group-data-[state=open]:text-primary" aria-hidden="true" /> </NavigationMenuPrimitive.Trigger> ); } function NavigationMenuContent({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) { return ( <NavigationMenuPrimitive.Content data-slot="navigation-menu-content" className={cn( "top-0 left-0 w-full p-2 md:absolute md:w-auto", "data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out 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", "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 group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow-block-sm group-data-[viewport=false]/navigation-menu:duration-200", "group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95", "**:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", className, )} {...props} /> ); } function NavigationMenuViewport({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) { return ( <div className="absolute top-full left-0 isolate z-50 flex w-full justify-center md:w-auto"> <NavigationMenuPrimitive.Viewport data-slot="navigation-menu-viewport" className={cn( "relative mt-2 h-(--radix-navigation-menu-viewport-height) w-full origin-[top_center] overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-block-sm md:w-(--radix-navigation-menu-viewport-width)", "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className, )} {...props} /> </div> ); } function NavigationMenuLink({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) { return ( <NavigationMenuPrimitive.Link data-slot="navigation-menu-link" className={cn( "flex flex-col gap-1 rounded-sm p-2.5 text-[13px] transition-colors outline-none hover:bg-secondary focus:bg-secondary focus-visible:ring-2 focus-visible:ring-ring/25 data-[active=true]:bg-primary/8 data-[active=true]:text-primary", "[&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", 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 transition-[width,transform] duration-200", "data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in", className, )} {...props} > <div className="h-0.5 w-[calc(100%-1rem)] rounded-full bg-primary" /> </NavigationMenuPrimitive.Indicator> ); } export { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuContent, NavigationMenuTrigger, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, navigationMenuTriggerStyle, };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Hubs</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink href="/hubs">Directory</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink href="/guestbook" className={navigationMenuTriggerStyle()}>
Guestbook
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>Below 768px the panel fills the width of the menu and links stack in one column; give panel contents md: widths for the wide layout.
Pass viewport={false} to render each panel under its own trigger instead of in the shared viewport. Put NavigationMenuIndicator last inside the list.