Navigation menu
Site navigation menu with a shared animated viewport.
Composition
The preferred shape for composing the installed primitive from its exported parts.
Parts
Exported compound parts from the installed source.
NavigationMenu ├── NavigationMenuList ├── NavigationMenuItem ├── NavigationMenuTrigger ├── NavigationMenuContent ├── NavigationMenuLink └── NavigationMenuViewport
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/navigation-menu.tsx. Install it on its own with the command above, or inspect the source below.
npx shadcn@latest add https://control-ui.dev/r/navigation-menu.jsonKnobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-button-* · 17 knobsHow the cascade resolves--cui-button-radius<length-percentage>var(--radius-control)--cui-button-gap<length-percentage>0.375rem--cui-button-icon<length>1rem--cui-button-bg<color>transparent--cui-button-bg-image*none--cui-button-foreground<color>var(--muted-foreground)--cui-button-hover-bg<color>oklch(from var(--foreground) l c h / 0.06)--cui-button-hover-foreground<color>var(--foreground)--cui-button-press-bg<color>var(--cui-button-hover-bg)--cui-button-press-scale<number>0.98--cui-button-active-bg<color>oklch(from var(--foreground) l c h / 0.08)--cui-button-active-foreground<color>var(--foreground)--cui-button-active-hover-bg<color>var(--cui-button-active-bg)--cui-button-shadow*0 0 transparent--cui-button-hover-shadow*var(--cui-button-shadow)--cui-button-press-shadow*var(--cui-button-hover-shadow)--cui-button-active-shadow*var(--cui-button-shadow)--cui-popup-* · 16 knobsHow the cascade resolves--cui-popup-radius*var(--radius-popover)--cui-popup-background<color>var(--popover)--cui-popup-foreground<color>var(--popover-foreground)--cui-popup-border-color<color>var(--border)--cui-popup-border-width<length>1px--cui-popup-shadow*var(--shadow-pop)--cui-popup-backdrop-filter*blur(var(--backdrop-blur-popover))--cui-popup-backdrop-background<color>oklch(from var(--foreground) l c h / var(--overlay-opacity))--cui-popup-item-radius<length-percentage>var(--radius-popup-item)--cui-popup-item-foreground<color>var(--foreground)--cui-popup-item-highlight-background<color>oklch(from var(--foreground) l c h / 0.06)--cui-popup-item-highlight-foreground<color>var(--cui-popup-item-foreground)--cui-popup-item-highlight-muted-foreground<color>var(--muted-foreground)--cui-popup-item-disabled-opacity<number>0.4--cui-popup-separator-color<color>var(--border)--cui-popup-shortcut-foreground<color>var(--muted-foreground)Installed dependencies
Support files installed with this primitive — shared ones arrive once with your first Control UI component. Public dependencies stay linked to their own pages.
src/registry/sources/control-ui/control-variants.tsSupportRaw code
This primitive's source and the support files it installs with
"use client";import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu";import type { ComponentProps, CSSProperties } from "react";import { buttonGapClass, controlSize } from "@/components/control-ui/control-variants";import type { ButtonKnobStyle } from "@/components/control-ui/knob-contracts/button-knobs";import type { PopupKnobStyle } from "@/components/control-ui/knob-contracts/popup-knobs";import { cn } from "@/components/control-ui/lib/cn";import { skinEffects, skinId } from "@/components/control-ui/skin";export type NavigationMenuProps = ComponentProps<"nav"> & { value?: string | null; defaultValue?: string | null; onValueChange?: (value: string | null) => void; orientation?: "horizontal" | "vertical"; delay?: number; closeDelay?: number;} & { style?: CSSProperties & PopupKnobStyle };export type NavigationMenuListProps = ComponentProps<"ul"> & { style?: CSSProperties & PopupKnobStyle };export type NavigationMenuItemProps = ComponentProps<"li"> & { value?: string;} & { style?: CSSProperties & PopupKnobStyle };export type NavigationMenuTriggerProps = Omit<ComponentProps<"button">, "style"> & { style?: CSSProperties & ButtonKnobStyle & PopupKnobStyle;};export type NavigationMenuContentProps = ComponentProps<"div"> & { style?: CSSProperties & PopupKnobStyle };export const navigationMenuLinkVariants = ["default", "compact"] as const;export type NavigationMenuLinkVariant = (typeof navigationMenuLinkVariants)[number];export type NavigationMenuLinkProps = Omit<ComponentProps<"a">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { active?: boolean; closeOnClick?: boolean; variant?: NavigationMenuLinkVariant;};export type NavigationMenuViewportProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };// Every item has its own Trigger and Content, but single viewport morphs between them rather than one popup per item.type RefinedLinkProps = NavigationMenuLinkProps & Pick<ComponentProps<typeof NavigationMenuPrimitive.Link>, "render">;export function NavigationMenu({ className, children, ...props }: NavigationMenuProps) { return ( <NavigationMenuPrimitive.Root data-control-ui="navigation-menu" data-control-family="popup" data-popup-kind="navigation-menu" data-slot="root" className={cn("relative flex max-w-max flex-1 items-center justify-center", className)} {...props} > {children} </NavigationMenuPrimitive.Root> );}export function NavigationMenuList({ className, ...props }: NavigationMenuListProps) { return ( <NavigationMenuPrimitive.List data-control-ui="navigation-menu" data-control-family="popup" data-popup-kind="navigation-menu" data-slot="list" className={cn("flex flex-1 list-none items-center justify-center gap-0.5", className)} {...props} /> );}export function NavigationMenuItem({ className, ...props }: NavigationMenuItemProps) { return ( <NavigationMenuPrimitive.Item data-control-ui="navigation-menu" data-control-family="popup" data-popup-kind="navigation-menu" data-slot="item" className={cn("relative", className)} {...props} /> );}export function NavigationMenuTrigger({ className, children, ...props }: NavigationMenuTriggerProps) { return ( <NavigationMenuPrimitive.Trigger data-control-ui="navigation-menu" data-control-family="button" data-slot="trigger" data-control="true" data-cursor="default" data-size="sm" data-variant="quiet" data-tone="neutral" data-shape="default" className={cn("inline-flex select-none items-center", controlSize({ size: "sm" }), buttonGapClass, className)} {...props} > {children} <NavigationMenuPrimitive.Icon data-control-ui="navigation-menu" data-control-family="popup" data-popup-kind="navigation-menu" data-slot="icon" > <svg viewBox="0 0 12 12" className="size-3" aria-hidden="true" fill="none"> <path d="M3 4.5 6 7.5 9 4.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" /> </svg> </NavigationMenuPrimitive.Icon> </NavigationMenuPrimitive.Trigger> );}export function NavigationMenuContent({ className, children, ...props }: NavigationMenuContentProps) { return ( <NavigationMenuPrimitive.Content data-control-ui="navigation-menu" data-control-family="popup" data-popup-kind="navigation-menu" data-slot="content" className={cn("h-full w-full p-[var(--popover-padding)]", className)} {...props} > {children} </NavigationMenuPrimitive.Content> );}export function NavigationMenuLink({ variant = "default", className, active, children, ...props }: RefinedLinkProps) { return ( <NavigationMenuPrimitive.Link data-control-ui="navigation-menu" data-popup-kind="navigation-menu" data-slot="link" data-control-family="popup" data-popup-part="navigation-link" data-variant={variant} active={active} className={cn( "block select-none px-3 py-2", variant === "compact" && "inline-flex h-[var(--control-h-sm)] items-center px-[calc(var(--padding-x)*0.75)] py-0", className, )} {...props} > {children} </NavigationMenuPrimitive.Link> );}export function NavigationMenuViewport({ className, ...props }: NavigationMenuViewportProps) { return ( <NavigationMenuPrimitive.Portal> {/* Portal escapes token-scoped ancestor — positioner re-asserts ACTIVE skin's scope. before:* strip bridges sideOffset gap so hover survives trigger→popup. */} <NavigationMenuPrimitive.Positioner data-control-ui="navigation-menu" data-control-family="popup" data-popup-kind="navigation-menu" data-slot="positioner" data-skin={skinId()} data-effects={skinEffects()} sideOffset={8} collisionPadding={{ top: 5, bottom: 5, left: 16, right: 16 }} collisionAvoidance={{ side: "none" }} className="z-[80] h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)] before:absolute before:inset-x-0 before:top-[-8px] before:h-2 before:content-['']" > <NavigationMenuPrimitive.Popup data-control-ui="navigation-menu" data-control-family="popup" data-popup-kind="navigation-menu" data-slot="popup" className="relative h-[var(--popup-height)] w-[var(--popup-width)]" > <NavigationMenuPrimitive.Viewport data-control-ui="navigation-menu" data-popup-kind="navigation-menu" data-slot="viewport" data-surface="floating" data-control-family="popup" data-popup-part="surface" className={cn("relative h-full w-full overflow-hidden", className)} {...props} /> </NavigationMenuPrimitive.Popup> </NavigationMenuPrimitive.Positioner> </NavigationMenuPrimitive.Portal> );}