Use when adding, debugging, or refactoring any shadcn ui menu surface : DropdownMenu (button-triggered popup), ContextMenu (right-click on an area), Menubar (horizontal File / Edit / View desktop-app bar), or NavigationMenu (site-wide nav with viewport + mega-menu content). Picks the correct primitive family from the interaction model rather than from the visual shape, composes the shared Radix Menu surface (Root / Trigger / Content / Item / Group / Label / Separator / CheckboxItem / RadioGroup / RadioItem / Sub / SubTrigger / SubContent / Portal / Shortcut), and wires CheckboxItem / RadioGroup state, sub- menus, item shortcuts, and the special NavigationMenu surface (List / Item / Trigger / Content / Link / Indicator / Viewport). Prevents the canonical menu failures : stripping `"use client"` from the menu file so DropdownMenu / ContextMenu / Menubar break with a hydration error, reaching for DropdownMenu when the UX is a right- click context menu (or vice versa), nesting MenubarSeparator outside MenubarCont
Use when adding, debugging, or refactoring any shadcn ui menu surface : DropdownMenu (button-triggered popup), ContextMenu (right-click on an area), Menubar (horizontal File / Edit / View desktop-app bar), or NavigationMenu (site-wide nav with viewport + mega-menu content). Picks the correct primitive family from the interaction model rather than from the visual shape, composes the shared Radix Menu surface (Root / Trigger / Content / Item / Group / Label / Separator / CheckboxItem / RadioGroup / RadioItem / Sub / SubTrigger / SubContent / Portal / Shortcut), and wires CheckboxItem / RadioGroup state, sub- menus, item shortcuts, and the special NavigationMenu surface (List / Item / Trigger / Content / Link / Indicator / Viewport). Prevents the canonical menu failures : stripping `"use client"` from the menu file so DropdownMenu / ContextMenu / Menubar break with a hydration error, reaching for DropdownMenu when the UX is a right- click context menu (or vice versa), nesting MenubarSeparator outside MenubarContent so Radix throws, composing NavigationMenuLink around a Next.js `<Link legacyBehavior>` so two anchors render, wrapping ContextMenuTrigger around a child without `asChild` so the child's own onContextMenu swallows the right-click, and rendering a SubContent without DropdownMenuPortal so it gets clipped by an overflow ancestor. Covers the four shadcn menu primitives verbatim from `apps/v4/registry/new-york-v4/ui/{dropdown-menu,context-menu,menubar, navigation-menu}.tsx`, the shared Radix Menu API (CheckboxItem + RadioGroup + RadioItem semantics, Sub / SubTrigger / SubContent composition, asChild on Trigger, controlled `open` / `onOpenChange` on Root and Sub roots, `data-state="open|closed"` styling hook), the per-primitive differences (ContextMenuTrigger as area wrapper with no explicit button, MenubarMenu wrapping each top-level menu, NavigationMenu's separate List / Viewport / Indicator surface and the `viewport={false}` opt-out), the v4 DropdownMenuContent auto- Portal, the Menubar default `align="start"` / `alignOffset=-4` / `sideOffset=8`, the NavigationMenuLink asChild + Next.js Link pattern (no `legacyBehavior` in app router), and the "icon + label + shortcut" composition convention for menu items. Keywords: shadcn dropdown menu, dropdown menu, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuShortcut, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuPortal, context menu, right click menu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, right click on row, right click on card, menubar, app menu, File Edit View, MenubarMenu, MenubarTrigger, MenubarContent, MenubarSub, MenubarShortcut, navigation menu, mega menu, site nav, header nav, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, when which menu, dropdown vs context menu, menubar vs navigation menu, which menu primitive, three dots menu, kebab menu, overflow menu, action menu, options menu, submenu, nested menu, checkbox in menu, radio in menu, keyboard shortcut in menu, menu hydration error, use client menu, right click swallowed, menu clipped by overflow.
license
MIT
compatibility
Designed for Claude Code. Requires shadcn ui evergreen-2026.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
shadcn ui : Menu Primitives
Four shadcn primitives share the Radix Menu surface : DropdownMenu, ContextMenu, Menubar, NavigationMenu. They look similar (a list of items in a popover-ish surface), but each maps to a different interaction model. Picking the right one is a one-question decision : how does the user open the menu? Composition of the body is then nearly identical (Item / CheckboxItem / RadioGroup / Sub / Separator / Shortcut), with NavigationMenu being the one exception that swaps the body for a list-based site-nav surface with a shared Viewport.
Quick Reference
Decision Tree : Which menu primitive?
Q1. How does the user OPEN the menu?
click a button (icon, "...", "Options", avatar) -> DropdownMenu
right-click (or long-press) on an area / row / canvas -> ContextMenu
click a label in a desktop-app top bar (File, Edit, View, Help, with
arrow-key navigation between top-level labels) -> Menubar
hover or click an item in a site / page header
nav, often opening a mega-menu of links -> NavigationMenu
ALWAYS pick from the interaction model first. NEVER pick by visual shape ; DropdownMenu and ContextMenu render almost identically but expose different a11y semantics (role="menu" opened from a button vs from contextmenu event).
Shared body surface (Dropdown / Context / Menubar)
Slot
Purpose
*Menu (Root)
State container ; open / onOpenChange / defaultOpen / modal
The popover surface ; align / side / sideOffset / alignOffset
*MenuGroup
Logical group ; pair with *MenuLabel for a header
*MenuLabel
Non-interactive heading inside a group
*MenuItem
Clickable row ; ; ; (Dropdown / Context)
onSelect
disabled
data-inset
*MenuCheckboxItem
Toggleable row ; checked / onCheckedChange
*MenuRadioGroup
Wraps RadioItems ; value / onValueChange
*MenuRadioItem
Mutually-exclusive choice inside a RadioGroup ; value
*MenuSeparator
Horizontal divider (a <hr>-shaped div)
*MenuShortcut
Right-aligned kbd-styled text (Cmd+K, etc.)
*MenuSub
Nested-submenu state container
*MenuSubTrigger
Opens the submenu ; auto-prepends a chevron-right
*MenuSubContent
The nested popover surface
The * prefix is one of DropdownMenu / ContextMenu / Menubar. The slot names, props, and composition rules are identical across all three.
NavigationMenu surface (different)
Slot
Purpose
NavigationMenu
Root ; viewport?: boolean (default true) toggles the shared Viewport
NavigationMenuList
The <ul> of top-level items
NavigationMenuItem
One <li> ; container for Trigger + Content (or a bare Link)
NavigationMenuTrigger
The hover/click target ; auto-appends a chevron-down
NavigationMenuContent
The mega-menu surface (rendered into Viewport)
NavigationMenuLink
A styled link ; asChild to wrap a Next.js <Link>
NavigationMenuIndicator
The optional pointing-triangle that tracks the active Trigger
NavigationMenuViewport
The shared sliding container (auto-rendered when viewport=true)
navigationMenuTriggerStyle
Exported cva helper for styling non-Trigger links to match
NavigationMenu does NOT use Portal / Group / Item / CheckboxItem / RadioGroup / Sub. It is a navigation primitive, not a command-menu primitive.
Five invariants
ALWAYS keep "use client" at the top of components/ui/{dropdown-menu,context-menu,menubar}.tsx. NEVER strip it ; the menu primitives use Radix client-only context. (NavigationMenu currently ships WITHOUT "use client" in the v4 registry source because the cva factory is module-level, but the file becomes a client boundary the moment it is imported into a Server Component. Let Next.js infer.)
ALWAYS pick the primitive from the interaction model (button-click / right-click / app-bar / site-nav). NEVER use DropdownMenu where the UX is right-click ; screen-reader users hear "menu" with no context of what was right-clicked.
ALWAYS render exactly ONE *MenuContent per *Menu Root. ALWAYS render exactly ONE MenubarContent per MenubarMenu. ALWAYS render exactly ONE NavigationMenuContent per NavigationMenuItem. NEVER conditionally render two and toggle which is shown.
ALWAYS place *MenuSeparator as a direct child of *MenuContent (or *MenuSubContent). NEVER nest it inside a *MenuItem or MenubarMenu ; Radix expects it at the same level as items.
ALWAYS wrap a non-default trigger via <*MenuTrigger asChild>. ALWAYS wrap a non-default link via <NavigationMenuLink asChild>. NEVER place two children inside asChild ; Radix Slot rejects multi-child.
DropdownMenu : button-triggered popup
The default action-menu primitive. Open via click on a button (icon-only <Button variant="ghost" size="icon">, a three-dot kebab, an avatar, a "Filter" pill). Used for table row actions, app header user menu, settings toggles, anywhere a button reveals a list.
DropdownMenuItem exposes a shadcn-specific inset?: boolean (left-pads to align with CheckboxItem / RadioItem rows) and variant?: "default" | "destructive" (data-variant drives the destructive red colouring).
DropdownMenuContent in the v4 source auto-wraps itself in DropdownMenuPortal. Use <DropdownMenuPortal> explicitly only when you need to override the container (shadow DOM, iframe, deliberately-constrained scope).
NEVER omit asChild on the Trigger when wrapping a <Button>. Without asChild, the Trigger renders its own default <button> and the <Button> becomes a nested button : invalid HTML and broken focus management.
ContextMenu : right-click area
For right-click (or long-press on touch) on a target area. Used on table rows, file thumbnails, canvas selections, anywhere "right-click to show options" is the UX pattern.
ContextMenuTrigger is an area wrapper, not a button. It wraps the element that should respond to right-click. Pass asChild if the child element is already a styled component.
There is no "click to open" affordance ; the user must know to right-click. Pair with a visible hint (a "..." icon, a tooltip) when discoverability matters.
Minimal pattern
"use client"import {
ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem,
} from"@/components/ui/context-menu"
<ContextMenu>
<ContextMenuTriggerasChild><divclassName="rounded border p-4">Right-click on this card</div></ContextMenuTrigger><ContextMenuContent><ContextMenuItem>Rename</ContextMenuItem><ContextMenuItem>Duplicate</ContextMenuItem><ContextMenuItemvariant="destructive">Delete</ContextMenuItem></ContextMenuContent>
</ContextMenu>
ALWAYS use asChild when the trigger child already renders a focusable / styled element (a Card, a Button, a row <tr>). Without asChild, ContextMenuTrigger renders an extra <span> wrapper that can break grid layout, table semantics, or flex sizing.
NEVER attach your own onContextMenu to the trigger's child without forwarding the event ; the child handler can preventDefault() and swallow the right-click before Radix sees it.
Menubar : desktop-app top bar
For desktop-style application menus : File, Edit, View, Help. One horizontal Menubar contains multiple MenubarMenu siblings, each owning its own Trigger + Content. Arrow keys move between top-level menus ; opening one menu and then hovering another auto-switches.
MenubarContent defaults : align="start", alignOffset={-4}, sideOffset={8} (verified from apps/v4/registry/new-york-v4/ui/menubar.tsx). The negative alignOffset aligns content with the Trigger's left edge.
ALWAYS wrap each top-level menu in its own <MenubarMenu>. NEVER place multiple MenubarTrigger / MenubarContent pairs as direct children of <Menubar> ; Radix uses the MenubarMenu wrapper to scope state per top-level entry.
Menubar is the right call when the app feels like a desktop application (an editor, a DAW, a CAD tool, a spreadsheet). For a website's main navigation, use NavigationMenu instead.
NavigationMenu : site-nav with viewport
For site / app header navigation : a horizontal list of top-level entries where each can open a panel of links (often a mega-menu with sections and descriptions). Different surface from the other three primitives.
NavigationMenu accepts a shadcn-specific viewport?: boolean prop (default true). When true, the Root auto-renders a <NavigationMenuViewport /> at the bottom and every NavigationMenuContent slides through it. When false, each NavigationMenuContent renders inline below its own Trigger (a per-item dropdown, like DropdownMenu but link-only).
ALWAYS use <NavigationMenuLink asChild> when wrapping a Next.js <Link>. NEVER use <Link legacyBehavior> together with <NavigationMenuLink asChild> ; modern Next.js Link no longer needs legacyBehavior / passHref (Next.js 13+ app router), and combining the two produces two nested anchor tags and a hydration mismatch.
For a per-item inline dropdown instead of the shared viewport, pass viewport={false} on <NavigationMenu>. Each <NavigationMenuContent> then renders directly below its Trigger via group-data-[viewport=false] Tailwind utilities (see apps/v4/registry/new-york-v4/ui/navigation-menu.tsx).
Shared pattern : sub-menus
Identical across Dropdown / Context / Menubar. NavigationMenu does NOT support sub-menus (use a nested NavigationMenuContent panel instead).
<DropdownMenuSub>
<DropdownMenuSubTrigger>More</DropdownMenuSubTrigger><DropdownMenuSubContent><DropdownMenuItem>Move to ...</DropdownMenuItem><DropdownMenuItem>Copy to ...</DropdownMenuItem></DropdownMenuSubContent>
</DropdownMenuSub>
*MenuSubTrigger auto-appends a <ChevronRightIcon className="ml-auto" /> in the shadcn v4 source. Do not add your own chevron.
ALWAYS render *MenuSubContent inside a *MenuSub ; the SubContent is automatically portaled (verified from apps/v4/registry/new-york-v4/ui/dropdown-menu.tsx).
NEVER nest a *MenuSub inside a NavigationMenuContent ; the primitive families are not interchangeable.
Identical names exist on ContextMenu (ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem) and Menubar (MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem).
ALWAYS pair CheckboxItem's checked with onCheckedChange, identical to the controlled-prop contract everywhere else in Radix. NEVER pass checked alone (the row becomes unresponsive).
Shared pattern : MenuShortcut
The shortcut hint at the right edge of a menu item. Pure layout, not interactive ; the actual keyboard shortcut must be implemented separately (a global key listener, a useHotkeys hook, a CommandDialog).
<DropdownMenuItem>
Save <DropdownMenuShortcut>Cmd+S</DropdownMenuShortcut>
</DropdownMenuItem>
NEVER expect <DropdownMenuShortcut> to bind the keystroke. It only renders the styled hint. See shadcn-syntax-command for the CommandDialog pattern that pairs visual shortcuts with real key handling.
a11y : roles per primitive
Each primitive maps to a different WAI-ARIA pattern. Radix wires the roles ; do not override.
Primitive
Trigger role
Content role
Item role
DropdownMenu
button with aria-haspopup="menu"
menu
menuitem / menuitemcheckbox / menuitemradio
ContextMenu
(none ; opens on contextmenu event on the trigger area)
menu
identical
Menubar
menuitem (inside the bar)
menu
identical
NavigationMenu
button with aria-haspopup="menu" (Trigger)
menu
link for NavigationMenuLink
Keyboard navigation is handled by Radix : arrow keys cycle items, Enter / Space selects, Esc closes, typing characters does type-ahead search. Do not re-implement these.
ALWAYS provide visible text content inside each *MenuItem. If the item is icon-only, wrap the icon's visual meaning in an <span className="sr-only"> so the role + name pair is valid. NEVER ship a menu item with no accessible name.
RSC and the "use client" directive
dropdown-menu.tsx, context-menu.tsx, and menubar.tsx all ship with "use client" at the top of the shadcn v4 registry source. This is non-negotiable :
Each uses Radix context internally.
State (open, data-state), focus trap, and Portal mounting all run client-side.
navigation-menu.tsx in the v4 registry source currently does NOT have "use client" as the first line, because the file's top-level cva() factory is module-level. Next.js treats the file as a client component when it is imported into a Server Component, via the radix-ui import boundary. If you migrate to a different React framework (Remix, vanilla Vite), add "use client" defensively.
ALWAYS keep "use client" in the Dropdown / Context / Menubar files. NEVER strip it ; the file imports radix-ui which requires a client boundary.
See shadcn-impl-rsc-vs-client-boundaries for the broader RSC contract.
Companion Skills
shadcn-syntax-button : the asChild pattern used by every *MenuTrigger and by NavigationMenuLink. Read first if asChild is unfamiliar.
shadcn-syntax-command : the cmdk command palette ; pairs MenuShortcut hints with actual key-binding logic via CommandDialog.
shadcn-errors-radix-controlled : the controlled-state failure catalogue across Radix primitives, including the Slot multi-child crash that bites *MenuTrigger asChild users.
shadcn-agents-component-selector : the validator that catches "DropdownMenu where ContextMenu fits" and other wrong-primitive choices.
Reference Links
references/methods.md : per-primitive primitive list (table form), prop signatures, defaults from the v4 registry source.
references/examples.md : six canonical recipes (DropdownMenu table-row actions, ContextMenu on a card, Menubar File/Edit/View, NavigationMenu mega-menu, CheckboxItem + RadioGroup, MenuShortcut composition).