Easy Shadcn
Components

Menu

Menu is the persistent, Ant Design-style navigation owner for easy-shadcn. One item tree covers action leaves, links, submenus, groups, separators, controlled or uncontrolled selection, and controlled or uncontrolled open state. It is not a renamed Dropdown Menu, Context Menu, Command, or Menubar.

Selected: overview

Installation

With the @easy-shadcn namespace configured:

pnpm dlx shadcn@latest add @easy-shadcn/menu

Or install via the full URL:

pnpm dlx shadcn@latest add https://easy-shadcn.vercel.app/r/menu.json

Menu owns its persistent navigation markup and state adapter directly. Its chevron icon packages are installed automatically.

The root <ul>, generated item tree, menu role, orientation, mode marker, and keyboard dispatcher are fixed by Menu at both type and runtime seams. Caller-owned accessible naming, refs, classes, styles, ordinary data-*, and native events remain available. A caller onKeyDown runs first; calling preventDefault() stops Menu's internal keyboard handling.

Basic use

The first use case needs only items. Add value / onValueChange when the parent owns selection.

import { Menu } from "@/components/easy/menu"

<Menu
  aria-label="Workspace navigation"
  items={[
    { key: "overview", label: "Overview" },
    { key: "activity", label: "Activity" },
  ]}
  value={section}
  onValueChange={setSection}
/>

Action leaves render buttons. Add href to render a real link. onSelect(key, item) runs for either kind and is also available when selectable={false}.

Nested items and modes

An item with children is a submenu. Open state uses the standard controlled triplet: openKeys, defaultOpenKeys, and onOpenKeysChange.

<Menu
  defaultOpenKeys={["team"]}
  items={[
    {
      key: "team",
      label: "Team",
      children: [
        { key: "members", label: "Members" },
        { key: "roles", label: "Roles" },
      ],
    },
  ]}
  mode="inline"
/>
ModeLayoutSubmenu behavior
verticalVertical rootPopup beside its parent; closes after leaf selection or outside pointer down.
horizontalHorizontal rootTop-level popup opens below; deeper popups open beside their parent.
inlineVertical rootChildren stay in document flow; caller-controlled or user-toggled open state persists after selection.

Selection

Single selection is the default and uses string | undefined. Add multiple to use string[] without changing the item model.

<Menu
  multiple
  defaultValue={["inbox"]}
  items={items}
  onValueChange={(keys) => console.log(keys)}
/>

selectable={false} disables selection ownership but preserves onSelect; use it when items dispatch actions without representing current navigation. Disabled leaves and submenus neither select nor open.

Item model

Every item needs a stable, unique key.

KindRequired fieldsOptional fieldsBehavior
Leafkey, labelhref, target, rel, icon, extra, disabled, class fieldsButton without href; anchor with href.
Submenukey, label, childrenicon, extra, disabled, class fieldsOwns one entry in openKeys.
Grouptype: "group", key, label, childrenclassName, labelClassNameNon-selectable label plus nested items.
Separatortype: "separator", keyclassNameSemantic separator.

icon, label, and extra are content slots. Their matching xxxClassName fields style only the wrapper owned by Menu.

Keyboard and accessibility

  • Menu uses one roving tab stop. Arrow Up / Down move through visible enabled items; Home / End jump to the first / last item.
  • Arrow Right opens a submenu and enters it. In horizontal mode Arrow Down opens a top-level submenu. Arrow Left or Escape closes a nested submenu and returns focus to its trigger.
  • Popup modes close on outside pointer down. Hidden submenu items are excluded from keyboard movement.
  • Pass aria-label or aria-labelledby when the surrounding navigation does not already provide a clear name.
  • Links stay anchors. Disabled links lose href, expose aria-disabled, and leave the tab sequence.

Props

PropTypeDefaultDescription
itemsMenuItem[]requiredRecursive item tree. Empty arrays are valid.
mode"vertical" | "horizontal" | "inline""vertical"Root layout and submenu placement model.
valuestring | string[]-Controlled selection; array form requires multiple.
defaultValuestring | string[]-Initial uncontrolled selection; array form requires multiple.
onValueChange(value) => void-Receives next single key or key array.
multiplebooleanfalseEnables multiple selection and array values.
selectablebooleantrueWhether leaf activation changes selection.
onSelect(key, item) => void-Runs after an enabled leaf activates, independent of selectable.
openKeysstring[]-Controlled open submenu keys.
defaultOpenKeysstring[][]Initial uncontrolled open submenu keys.
onOpenKeysChange(keys: string[]) => void-Receives the complete next open-key set.
itemClassNameClassValue-Class merged into every leaf or submenu trigger.
iconClassNameClassValue-Class merged into every icon wrapper.
labelClassNameClassValue-Class merged into every item label wrapper.
extraClassNameClassValue-Class merged into every extra wrapper.
submenuClassNameClassValue-Class merged into every submenu list.
classNameClassValue-Root menu class. Other compatible caller-owned list props are forwarded.

Which menu component?

  • Use Menu for persistent application or site navigation with selected and expanded state.
  • Use DropdownMenu for a trigger-attached temporary action list.
  • Use ContextMenu for a right-click or long-press action list.
  • Use Command Palette for temporary searchable quick actions. It is not selection navigation.

Dropdown Menu and Context Menu remain separate components because their trigger, coordinates, focus restoration, dismissal, and portal lifecycles differ from persistent Menu.

On this page