# Menu

> Present a compact list of actions with keyboard navigation.

Source: https://www.honestui.com/docs/components/menu

```tsx
import {
  Pause as PauseIcon,
  Play as PlayIcon,
  SkipBack as SkipBackIcon,
  SkipForward as SkipForwardIcon,
  Trash as TrashIcon,
} from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuCheckboxItem,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuPopup,
  MenuRadioGroup,
  MenuRadioItem,
  MenuSeparator,
  MenuShortcut,
  MenuSub,
  MenuSubPopup,
  MenuSubTrigger,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function MenuDemo() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" />}>
        Open menu
      </MenuTrigger>
      <MenuPopup>
        <MenuGroup>
          <MenuGroupLabel>Playback</MenuGroupLabel>
          <MenuItem>
            <PlayIcon className="opacity-72" />
            Play
            <MenuShortcut>⌘P</MenuShortcut>
          </MenuItem>
          <MenuItem disabled>
            <PauseIcon className="opacity-72" />
            Pause
            <MenuShortcut>⇧⌘P</MenuShortcut>
          </MenuItem>
          <MenuItem>
            <SkipBackIcon className="opacity-72" />
            Previous
            <MenuShortcut>⌘[</MenuShortcut>
          </MenuItem>
          <MenuItem>
            <SkipForwardIcon className="opacity-72" />
            Next
            <MenuShortcut>⌘]</MenuShortcut>
          </MenuItem>
        </MenuGroup>
        <MenuSeparator />
        <MenuCheckboxItem>Shuffle</MenuCheckboxItem>
        <MenuCheckboxItem>Repeat</MenuCheckboxItem>
        <MenuCheckboxItem disabled>Enhanced Audio</MenuCheckboxItem>
        <MenuSeparator />
        <MenuGroup>
          <MenuGroupLabel>Sort by</MenuGroupLabel>
          <MenuRadioGroup>
            <MenuRadioItem value="artist">Artist</MenuRadioItem>
            <MenuRadioItem value="album">Album</MenuRadioItem>
            <MenuRadioItem value="title">Title</MenuRadioItem>
          </MenuRadioGroup>
        </MenuGroup>
        <MenuSeparator />
        <MenuSub>
          <MenuSubTrigger>Add to Playlist</MenuSubTrigger>
          <MenuSubPopup>
            <MenuItem>Jazz</MenuItem>
            <MenuSub>
              <MenuSubTrigger>Rock</MenuSubTrigger>
              <MenuSubPopup>
                <MenuItem>Hard Rock</MenuItem>
                <MenuItem>Soft Rock</MenuItem>
                <MenuItem>Classic Rock</MenuItem>
                <MenuSeparator />
                <MenuItem>Metal</MenuItem>
                <MenuItem>Punk</MenuItem>
                <MenuItem>Grunge</MenuItem>
                <MenuItem>Alternative</MenuItem>
                <MenuItem>Indie</MenuItem>
                <MenuItem>Electronic</MenuItem>
              </MenuSubPopup>
            </MenuSub>
            <MenuItem>Pop</MenuItem>
          </MenuSubPopup>
        </MenuSub>
        <MenuSeparator />
        <MenuItem variant="destructive">
          <TrashIcon />
          Delete
          <MenuShortcut>⌘⌫</MenuShortcut>
        </MenuItem>
      </MenuPopup>
    </Menu>
  )
}

```

## Overview [#overview]

Use Menu for a compact list of actions or choices opened from a trigger: overflow actions behind an icon button, account menus, row actions, editor commands, and grouped command lists. Opening the menu is a deliberate act, so its contents can be dense — but the actions themselves should still be reachable elsewhere if they are frequent or essential.

Do not use a menu for primary navigation when links should stay visible and crawlable, and do not use one when people must compare options side by side; menus hide everything except the highlighted item. For text entry or filtering over long lists, use Autocomplete or Combobox instead.

## Anatomy [#anatomy]

A menu has a trigger, popup, items, optional groups with labels, separators, checkbox items, radio groups, shortcut hints, and nested submenus. `MenuPopup` renders its own portal and positioner, so alignment props like `align` and `sideOffset` sit directly on it. Each part is defined in `menu.tsx` and exported as a named PascalCase component; `dropdown-menu.tsx` re-exports the same parts under `DropdownMenu*` names.

## Behavior [#behavior]

**Opening.** Clicking, pressing <kbd>Enter</kbd> or <kbd>Space</kbd>, or pressing <kbd>Arrow Down</kbd> on the trigger opens the menu and moves focus to the first item. By default the menu is modal while open: page scroll locks and pointer interaction outside is disabled. Set `openOnHover` on the trigger to also open after a short hover delay.

**Navigating.** <kbd>Arrow Up</kbd> and <kbd>Arrow Down</kbd> move the highlight, wrapping around by default (`loopFocus`); <kbd>Home</kbd> and <kbd>End</kbd> jump to the first and last item. Typing letters jumps straight to matching item labels — the typeahead resets shortly after you stop typing. Pass `label` to an item whose visible text differs from what should match.

**Activating.** <kbd>Enter</kbd> and <kbd>Space</kbd> activate the highlighted item, and items close the menu after activating by default (`closeOnClick`). Checkbox and radio items keep their checked state visible through the leading indicator column. Set `closeOnClick={false}` on items that toggle something without dismissing their context.

**Closing.** <kbd>Escape</kbd> closes the menu and returns focus to the trigger. Outside clicks close it as well. Pass `finalFocus` on the popup to send focus elsewhere on close.

**Submenus.** Highlighting a submenu trigger and pressing <kbd>Arrow Right</kbd> opens it (<kbd>Arrow Left</kbd> mirrors in right-to-left layouts); <kbd>Escape</kbd> closes only the child menu, leaving the parent open.

**Scrolling.** `MenuPopup` limits itself to Base UI's available viewport height and scrolls vertically when its content is taller, with `overscroll-contain` so wheel and touch scrolling never chains to the page. To add a smaller product-specific limit, pass a `max-height` class that keeps `--available-height` as the upper bound, such as `max-h-[min(18rem,var(--available-height))]`.

## Accessibility [#accessibility]

The trigger announces itself with `aria-haspopup="menu"` and `aria-expanded`; the popup carries `role="menu"` and is labelled by the trigger. All navigation works with arrows, typeahead, Home/End, Enter, and Space, including items outside the initially visible scroll area — scrolling to reach them happens inside the popup, not the page.

Checkbox and radio items expose their checked state to assistive technology; use them only when the checked state genuinely matters inside the menu. Disabled items stay focusable but inert and are dimmed through tokens rather than removed, so people can find them and hear that they exist.

Colors come from theme tokens, so highlights, disabled states, and the destructive variant adapt to dark mode automatically. Item padding uses logical properties, so indentation and shortcut alignment mirror in right-to-left locales; the submenu chevron does not flip automatically, so verify direction under `dir="rtl"`. Long labels wrap within the item rather than widening the popup past the viewport, since horizontal overflow is hidden.

If a list becomes difficult to scan even with scrolling and typeahead, use Autocomplete or Combobox instead — search beats forcing people through a very long action menu.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;menu&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui/react
      ```

      
        Import the following variables into your CSS file
      

      ```css
      @theme inline {
        --color-destructive-foreground: var(--destructive-foreground);
      }

      :root {
        --destructive-foreground: oklch(0.505 0.213 27.518);
      }

      .dark {
        --destructive-foreground: oklch(0.704 0.191 22.216);
      }
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/menu.tsx

```tsx
"use client"

import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { Check as CheckIcon, ChevronRight as ChevronRightIcon } from "honestui/icons"

import { cn } from "@/lib/utils"

const Menu = MenuPrimitive.Root

const MenuPortal = MenuPrimitive.Portal

function MenuTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
  return (
    <MenuPrimitive.Trigger
      data-slot="menu-trigger"
      className={cn(
        "data-pressed:bg-[var(--hui-color-background-base-primary-hover)]",
        className
      )}
      {...props}
    />
  )
}

function MenuPopup({
  className,
  sideOffset = 4,
  align = "center",
  alignOffset = 0,
  ...props
}: MenuPrimitive.Popup.Props & {
  align?: MenuPrimitive.Positioner.Props["align"]
  sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]
  alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]
}) {
  return (
    <MenuPrimitive.Portal>
      <MenuPrimitive.Positioner
        data-slot="menu-positioner"
        className="z-[var(--hui-z-index-portal)]"
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
      >
        <MenuPrimitive.Popup
          data-slot="menu-popup"
          className={cn(
            "box-border max-h-(--available-height) min-w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto overscroll-contain rounded-[var(--hui-radius-2)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] p-[var(--hui-space-2)] text-[var(--hui-color-foreground-base-primary)] shadow-[var(--hui-shadow-soft)] outline-none [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] [transition:opacity_var(--hui-duration-fast)_var(--hui-ease-out)] focus:outline-none focus-visible:outline-none data-ending-style:opacity-0 data-starting-style:opacity-0 motion-safe:[transition:opacity_var(--hui-duration-fast)_var(--hui-ease-out),transform_var(--hui-duration-fast)_var(--hui-ease-out)] motion-safe:data-ending-style:scale-[0.97] motion-safe:data-starting-style:scale-[0.97]",
            className
          )}
          {...props}
        />
      </MenuPrimitive.Positioner>
    </MenuPrimitive.Portal>
  )
}

function MenuGroup(props: MenuPrimitive.Group.Props) {
  return <MenuPrimitive.Group data-slot="menu-group" {...props} />
}

function MenuItem({
  className,
  inset,
  variant = "default",
  ...props
}: MenuPrimitive.Item.Props & {
  inset?: boolean
  variant?: "default" | "destructive"
}) {
  return (
    <MenuPrimitive.Item
      data-slot="menu-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "relative flex items-center gap-[var(--hui-space-3)] p-[var(--hui-space-3)] outline-none select-none [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] aria-disabled:pointer-events-none aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:cursor-pointer data-highlighted:rounded-[var(--hui-radius-2)] data-highlighted:bg-[var(--hui-color-background-base-primary-hover)] data-inset:ps-8 data-[variant=destructive]:text-[var(--hui-color-foreground-danger-primary)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:text-[var(--hui-color-foreground-base-secondary)] [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function MenuCheckboxItem({
  className,
  children,
  checked,
  ...props
}: MenuPrimitive.CheckboxItem.Props) {
  return (
    <MenuPrimitive.CheckboxItem
      data-slot="menu-checkbox-item"
      className={cn(
        "relative grid grid-cols-[1rem_1fr] items-center gap-[var(--hui-space-3)] p-[var(--hui-space-3)] outline-none in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] aria-disabled:pointer-events-none aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:cursor-pointer data-highlighted:rounded-[var(--hui-radius-2)] data-highlighted:bg-[var(--hui-color-background-base-primary-hover)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:text-[var(--hui-color-foreground-base-secondary)] [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      checked={checked}
      {...props}
    >
      <MenuPrimitive.CheckboxItemIndicator className="col-start-1">
        <CheckIcon />
      </MenuPrimitive.CheckboxItemIndicator>
      <span className="col-start-2">{children}</span>
    </MenuPrimitive.CheckboxItem>
  )
}

function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
  return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />
}

function MenuRadioItem({
  className,
  children,
  ...props
}: MenuPrimitive.RadioItem.Props) {
  return (
    <MenuPrimitive.RadioItem
      data-slot="menu-radio-item"
      className={cn(
        "relative grid grid-cols-[1rem_1fr] items-center gap-[var(--hui-space-3)] p-[var(--hui-space-3)] outline-none in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] aria-disabled:pointer-events-none aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:cursor-pointer data-highlighted:rounded-[var(--hui-radius-2)] data-highlighted:bg-[var(--hui-color-background-base-primary-hover)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:text-[var(--hui-color-foreground-base-secondary)] [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      <MenuPrimitive.RadioItemIndicator className="col-start-1">
        <CheckIcon />
      </MenuPrimitive.RadioItemIndicator>
      <span className="col-start-2">{children}</span>
    </MenuPrimitive.RadioItem>
  )
}

function MenuGroupLabel({
  className,
  inset,
  ...props
}: MenuPrimitive.GroupLabel.Props & {
  inset?: boolean
}) {
  return (
    <MenuPrimitive.GroupLabel
      data-slot="menu-label"
      data-inset={inset}
      className={cn(
        "px-[var(--hui-space-3)] py-[var(--hui-space-2)] [font-size:var(--hui-font-size-mini)] [font-weight:var(--hui-font-weight-medium)] data-inset:ps-8",
        className
      )}
      {...props}
    />
  )
}

function MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
  return (
    <MenuPrimitive.Separator
      data-slot="menu-separator"
      className={cn(
        "my-[var(--hui-space-2)] h-px bg-[var(--hui-color-border-base-primary)] [margin-inline:calc(var(--hui-space-3)*-1)]",
        className
      )}
      {...props}
    />
  )
}

function MenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="menu-shortcut"
      className={cn(
        "ms-auto flex items-center text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)]",
        className
      )}
      {...props}
    />
  )
}

function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) {
  return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />
}

function MenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: MenuPrimitive.SubmenuTrigger.Props & {
  inset?: boolean
}) {
  return (
    <MenuPrimitive.SubmenuTrigger
      data-slot="menu-sub-trigger"
      data-inset={inset}
      className={cn(
        "relative flex items-center gap-[var(--hui-space-3)] p-[var(--hui-space-3)] outline-none [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] aria-disabled:pointer-events-none aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:cursor-pointer data-highlighted:rounded-[var(--hui-radius-2)] data-highlighted:bg-[var(--hui-color-background-base-primary-hover)] data-popup-open:cursor-pointer data-popup-open:rounded-[var(--hui-radius-2)] data-popup-open:bg-[var(--hui-color-background-base-primary-hover)] data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:text-[var(--hui-color-foreground-base-secondary)] [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      {children}
      <ChevronRightIcon className="ms-auto" />
    </MenuPrimitive.SubmenuTrigger>
  )
}

function MenuSubPopup({
  className,
  sideOffset = 0,
  alignOffset = -4,
  align = "start",
  ...props
}: MenuPrimitive.Popup.Props & {
  align?: MenuPrimitive.Positioner.Props["align"]
  sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]
  alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]
}) {
  return (
    <MenuPopup
      className={className}
      sideOffset={sideOffset}
      align={align}
      alignOffset={alignOffset}
      data-slot="menu-sub-content"
      {...props}
    />
  )
}

export {
  Menu,
  Menu as DropdownMenu,
  MenuPortal,
  MenuPortal as DropdownMenuPortal,
  MenuTrigger,
  MenuTrigger as DropdownMenuTrigger,
  MenuPopup,
  MenuPopup as DropdownMenuContent,
  MenuGroup,
  MenuGroup as DropdownMenuGroup,
  MenuItem,
  MenuItem as DropdownMenuItem,
  MenuCheckboxItem,
  MenuCheckboxItem as DropdownMenuCheckboxItem,
  MenuRadioGroup,
  MenuRadioGroup as DropdownMenuRadioGroup,
  MenuRadioItem,
  MenuRadioItem as DropdownMenuRadioItem,
  MenuGroupLabel,
  MenuGroupLabel as DropdownMenuLabel,
  MenuSeparator,
  MenuSeparator as DropdownMenuSeparator,
  MenuShortcut,
  MenuShortcut as DropdownMenuShortcut,
  MenuSub,
  MenuSub as DropdownMenuSub,
  MenuSubTrigger,
  MenuSubTrigger as DropdownMenuSubTrigger,
  MenuSubPopup,
  MenuSubPopup as DropdownMenuSubContent,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Menu,
  MenuCheckboxItem,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuPopup,
  MenuRadioGroup,
  MenuRadioItem,
  MenuSeparator,
  MenuSub,
  MenuSubPopup,
  MenuSubTrigger,
  MenuTrigger,
} from "@/components/ui/menu";
```

```tsx
<Menu>
  <MenuTrigger>Open</MenuTrigger>
  <MenuPopup align="start" sideOffset={4}>
    <MenuItem>Profile</MenuItem>
    <MenuSeparator />

    <MenuGroup>
      <MenuGroupLabel>Playback</MenuGroupLabel>
      <MenuItem>Play</MenuItem>
      <MenuItem>Pause</MenuItem>
    </MenuGroup>

    <MenuSeparator />

    <MenuCheckboxItem>Shuffle</MenuCheckboxItem>
    <MenuCheckboxItem>Repeat</MenuCheckboxItem>

    <MenuSeparator />

    <MenuGroup>
      <MenuGroupLabel>Sort by</MenuGroupLabel>
      <MenuRadioGroup>
        <MenuRadioItem>Artist</MenuRadioItem>
        <MenuRadioItem>Album</MenuRadioItem>
        <MenuRadioItem>Title</MenuRadioItem>
      </MenuRadioGroup>
    </MenuGroup>

    <MenuSeparator />

    <MenuSub>
      <MenuSubTrigger>Add to playlist</MenuSubTrigger>
      <MenuSubPopup>
        <MenuItem>Jazz</MenuItem>
        <MenuItem>Rock</MenuItem>
      </MenuSubPopup>
    </MenuSub>
  </MenuPopup>
</Menu>
```

`MenuShortcut` renders a decorative hint span aligned to the end edge; it does not register a keyboard shortcut by itself.

## Don't do this [#dont-do-this]

### Navigating with onClick [#navigating-with-onclick]

```tsx
// Bad
<MenuItem onClick={() => router.push("/docs")}>Docs</MenuItem>
```

```tsx
// Good
<MenuItem render={<Link href="/docs" />}>Docs</MenuItem>
```

The bad version is a div pretending to be a link: it cannot be opened in a new tab with middle-click or <kbd>Cmd</kbd> click, shows no URL on hover, and assistive technology announces a command rather than a destination. Rendering through `Link` keeps real anchor semantics while inheriting the item's styling and keyboard behavior.

### Fake keyboard shortcuts [#fake-keyboard-shortcuts]

```tsx
// Bad
<MenuItem onClick={() => setTheme("dark")}>
  Dark theme
  <MenuShortcut>⌘D</MenuShortcut>
</MenuItem>
```

```tsx
// Good
useHotkey("mod+d", () => setTheme("dark"))

<MenuItem onClick={() => setTheme("dark")}>
  Dark theme
  <MenuShortcut>⌘D</MenuShortcut>
</MenuItem>
```

`MenuShortcut` is purely visual. Advertising a shortcut that does nothing breaks trust twice: once when it fails inside the app, and once when someone tries it from muscle memory outside the menu. Register the binding globally before printing it.

### Text fields inside the popup [#text-fields-inside-the-popup]

```tsx
// Bad
<MenuPopup>
  <Input placeholder="Filter actions" />
  <MenuItem>Rename</MenuItem>
</MenuPopup>
```

```tsx
// Good
<Combobox items={actions}>
  {/* Filterable list */}
</Combobox>
```

Menus intercept keystrokes for typeahead and arrow-key navigation, so a text input inside the popup fights the component for every keypress and the highlight jumps as people type. When a list needs filtering, use a component built around text entry.

## Examples [#examples]

### Open on Hover [#open-on-hover]

Hover opening suits toolbar-style triggers; keep click working by default elsewhere because hover menus misfire on touch screens.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function MenuHoverDemo() {
  return (
    <Menu>
      <MenuTrigger openOnHover render={<Button variant="secondary" />}>
        Hover me
      </MenuTrigger>
      <MenuPopup>
        <MenuItem>Item one</MenuItem>
        <MenuItem>Item two</MenuItem>
      </MenuPopup>
    </Menu>
  )
}

```

### With Checkbox [#with-checkbox]

Toggleable settings keep the menu open context while showing state.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuCheckboxItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function MenuCheckboxDemo() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" />}>
        Open menu
      </MenuTrigger>
      <MenuPopup>
        <MenuCheckboxItem defaultChecked>Auto save</MenuCheckboxItem>
        <MenuCheckboxItem>Notifications</MenuCheckboxItem>
      </MenuPopup>
    </Menu>
  )
}

```

### With Radio Group [#with-radio-group]

One selected value per group, indicated in the leading column.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuPopup,
  MenuRadioGroup,
  MenuRadioItem,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function MenuRadioGroupDemo() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" />}>
        Open menu
      </MenuTrigger>
      <MenuPopup>
        <MenuRadioGroup defaultValue="system">
          <MenuRadioItem value="light">Light</MenuRadioItem>
          <MenuRadioItem value="dark">Dark</MenuRadioItem>
          <MenuRadioItem value="system">System</MenuRadioItem>
        </MenuRadioGroup>
      </MenuPopup>
    </Menu>
  )
}

```

### With Link [#with-link]

Items rendered as real links keep middle-click and new-tab behavior.

```tsx
import Link from "next/link"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function MenuLinkDemo() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" />}>
        Open menu
      </MenuTrigger>
      <MenuPopup>
        <MenuItem render={<Link href="/docs" />}>Docs</MenuItem>
        <MenuItem render={<Link href="/particles" />}>
          Particles
        </MenuItem>
      </MenuPopup>
    </Menu>
  )
}

```

### With Group Label [#with-group-label]

Labels give screen readers group names and sighted users scan lines.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuSeparator,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function MenuGroupLabelsDemo() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" />}>
        Open menu
      </MenuTrigger>
      <MenuPopup>
        <MenuItem disabled>Account</MenuItem>
        <MenuItem>Profile</MenuItem>
        <MenuItem>Billing</MenuItem>
        <MenuSeparator />
        <MenuItem disabled>Support</MenuItem>
        <MenuItem>Docs</MenuItem>
        <MenuItem>Contact</MenuItem>
      </MenuPopup>
    </Menu>
  )
}

```

### Icon-only trigger [#icon-only-trigger]

An icon-only trigger needs an explicit `aria-label` — the menu's accessible name comes from it.

```tsx
import { Ellipsis as MoreHorizontalIcon } from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "@/components/honest-ui/ui/menu"

export function MenuProjectActions() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" size="icon" />} aria-label="Project actions"><MoreHorizontalIcon /></MenuTrigger>
      <MenuPopup>
        <MenuItem>Rename</MenuItem>
        <MenuItem>Duplicate</MenuItem>
        <MenuItem>Archive</MenuItem>
      </MenuPopup>
    </Menu>
  )
}

```

### Scrollable Menu [#scrollable-menu]

A product-specific cap keeps long lists manageable while `--available-height` remains the ceiling.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

const workspaces = [
  "Acme Studio",
  "Atlas Research",
  "Beacon Labs",
  "Cedar Health",
  "Field Notes",
  "Good Measure",
  "Harbor Finance",
  "Honest UI",
  "Juniper Works",
  "Northstar",
  "Open Assembly",
  "Signal House",
]

export function MenuScrollable() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" />}>
        Switch workspace
      </MenuTrigger>
      <MenuPopup
        align="start"
        className="max-h-[min(18rem,var(--available-height))] min-w-48"
      >
        <MenuGroup>
          <MenuGroupLabel>Workspaces</MenuGroupLabel>
          {workspaces.map((workspace) => (
            <MenuItem key={workspace}>{workspace}</MenuItem>
          ))}
        </MenuGroup>
      </MenuPopup>
    </Menu>
  )
}

```

### Nested Menu [#nested-menu]

Submenus preserve arrow-key navigation and close independently on Escape. Keep nesting shallow: each level costs precision and memory.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuSub,
  MenuSubPopup,
  MenuSubTrigger,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function MenuNestedDemo() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="secondary" />}>
        Open menu
      </MenuTrigger>
      <MenuPopup>
        <MenuItem>Item one</MenuItem>
        <MenuSub>
          <MenuSubTrigger>More</MenuSubTrigger>
          <MenuSubPopup>
            <MenuItem>Sub item A</MenuItem>
            <MenuItem>Sub item B</MenuItem>
          </MenuSubPopup>
        </MenuSub>
        <MenuItem>Item two</MenuItem>
      </MenuPopup>
    </Menu>
  )
}

```

### Open a Dialog [#open-a-dialog]

Actions that need more space or confirmation hand off to a Dialog on selection.

```tsx
"use client"

import * as React from "react"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Dialog,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPopup,
  DialogTitle,
} from "@/components/honest-ui/ui/dialog"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function DialogFromMenuDemo() {
  const [dialogOpen, setDialogOpen] = React.useState(false)
  return (
    <>
      <Menu>
        <MenuTrigger render={<Button variant="secondary" />}>
          Open menu
        </MenuTrigger>
        <MenuPopup align="start">
          <MenuItem onClick={() => setDialogOpen(true)}>
            Open dialog
          </MenuItem>
        </MenuPopup>
      </Menu>
      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>Settings</DialogTitle>
            <DialogDescription>
              Change your preferences
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />}>
              Close
            </DialogClose>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
    </>
  )
}

```

## API reference [#api-reference]

All parts forward their matching Base UI Menu props from `@base-ui/react`. Honest UI adds:

| Part                               | Props                                | Default              |
| ---------------------------------- | ------------------------------------ | -------------------- |
| `MenuPopup` / `MenuSubPopup`       | `sideOffset`, `align`, `alignOffset` | `4`, `center`, `0`   |
| `MenuItem`                         | `inset`, `variant`                   | `false`, `"default"` |
| `MenuGroupLabel`, `MenuSubTrigger` | `inset`                              | `false`              |

The root accepts controlled `open` / `onOpenChange`, `loopFocus` (wraps arrow navigation, `true` by default), `modal` (scroll lock and outside-pointer blocking, `true` by default), and `orientation`. Items accept `disabled`, `closeOnClick` (`true` by default), and a `label` override for typeahead matching. The popup accepts `finalFocus` for post-close focus placement. `MenuSubPopup` tightens the offsets so submenus hug their trigger. Every `Menu*` export is also aliased as `DropdownMenu*` from the same file.

See the [Base UI Menu API](https://base-ui.com/react/components/menu#api-reference) for focus, submenu, checkbox, and radio behavior.
