Navigation

Navigation Menu.

A header menu whose triggers open panels of links in a shared viewport, with an orange indicator under the open trigger.

Installation#

With the CLI

$npx lanterncn add navigation-menu

This 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

  1. Install the dependencies

    npm install radix-ui class-variance-authority lucide-react
  2. Copy 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,
    };
  3. Update the import paths to match your project

    The source imports cn from @/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.