Navigation Menu
A responsive collection of links and optional disclosure panels for site navigation.
Preview
Example code
Playground
viewportRenders panels inside the animated shared viewport.
Usage
tsx
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "earthling-ui/navigation-menu";
export function Example() {
return (
<>
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Explore</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink href="/getting-started">
Getting started
</NavigationMenuLink>
</NavigationMenuContent>
<NavigationMenuIndicator />
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</>
);
}
- Every destination should use a real link with meaningful text and a valid URL.
- Keep hover highlights instant because users scan navigation frequently.
- Disable the shared viewport when each panel needs independent positioning.
Installation
Start with the project setup for React and Tailwind CSS 4. Both paths use the same component and theme.
Import package
Own the source
bash
npm install earthling-uiUse the component import shown above. Package updates keep the implementation current.
API reference
Declarations from the current library build, including inherited primitive props. Playground controls above show selected options; they are not the complete API.
tsx
// Generated by dts-bundle-generator v9.5.1
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { ComponentProps } from 'react';
export declare function NavigationMenu({ className, children, viewport, ...props }: ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}): import("react").JSX.Element;
export declare function NavigationMenuList({ className, ...props }: ComponentProps<typeof NavigationMenuPrimitive.List>): import("react").JSX.Element;
export declare function NavigationMenuItem({ className, ...props }: ComponentProps<typeof NavigationMenuPrimitive.Item>): import("react").JSX.Element;
export declare const navigationMenuTriggerStyle: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
export declare function NavigationMenuTrigger({ className, children, ...props }: ComponentProps<typeof NavigationMenuPrimitive.Trigger>): import("react").JSX.Element;
export declare function NavigationMenuContent({ className, ...props }: ComponentProps<typeof NavigationMenuPrimitive.Content>): import("react").JSX.Element;
export declare function NavigationMenuViewport({ className, ...props }: ComponentProps<typeof NavigationMenuPrimitive.Viewport>): import("react").JSX.Element;
export declare function NavigationMenuLink({ className, ...props }: ComponentProps<typeof NavigationMenuPrimitive.Link>): import("react").JSX.Element;
export declare function NavigationMenuIndicator({ className, ...props }: ComponentProps<typeof NavigationMenuPrimitive.Indicator>): import("react").JSX.Element;
export {};Runtime dependencies: @radix-ui/react-navigation-menu, class-variance-authority, cnfast.
Source
Use the eject command to copy this implementation with its required helpers and project-specific imports.
View Navigation Menu implementation
tsx
"use client";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { cn } from "@/utils/cn";
import type { ComponentProps } from "react";
function NavigationMenu({
className,
children,
viewport = true,
...props
}: 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
}: ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group m-0 flex flex-1 list-none items-center justify-center gap-1 p-0",
className,
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}: ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max cursor-pointer items-center justify-center rounded-(--radius-control) bg-transparent px-4 py-2 text-sm font-medium hover:bg-(--scheme-tint)/5 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-outline disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-(--scheme-tint)/10 [--scheme-tint:var(--color-primary)]",
);
function NavigationMenuTrigger({
className,
children,
...props
}: ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<i
className="relative top-px ms-1 size-3 transition-transform duration-150 ease-out icon-[lucide--chevron-down] group-data-[state=open]:rotate-180 motion-reduce:transition-none"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}: ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"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-4 data-[motion=from-start]:slide-in-from-left-4 data-[motion=to-end]:slide-out-to-right-4 data-[motion=to-start]:slide-out-to-left-4 top-0 start-0 w-full p-2 duration-150 ease-out motion-reduce:animate-none md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-background group-data-[viewport=false]/navigation-menu:text-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:max-h-[calc(100dvh-2rem)] group-data-[viewport=false]/navigation-menu:overflow-auto group-data-[viewport=false]/navigation-menu:rounded-xl group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow",
className,
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}: ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute start-0 top-full isolate z-50 flex max-w-[calc(100vw-2rem)] justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"relative mt-1.5 h-(--radix-navigation-menu-viewport-height) max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100vw-2rem)] origin-top overflow-auto rounded-xl border bg-background text-foreground shadow duration-150 ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 motion-reduce:animate-none md:w-(--radix-navigation-menu-viewport-width)",
className,
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}: ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"flex flex-col gap-1 rounded-lg p-2 text-sm outline-hidden hover:bg-(--scheme-tint)/5 focus-visible:ring-2 focus-visible:ring-outline data-[active=true]:bg-(--scheme-tint)/10 [--scheme-tint:var(--color-primary)] [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: 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 duration-150 ease-out data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in motion-reduce:animate-none",
className,
)}
{...props}
>
<div className="bg-muted relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};