<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# Menu

A list of actions that opens from a button, from a right-click or long press, or from a menubar, with one API for all three. Items take a shortcut (shown with `Kbd`), `tone="destructive"` and `href`, and there are checkbox items, radio groups, labelled groups, submenus and separators. Below 640px a dropdown is a bottom sheet; a context menu opens at the pointer at every width.

```tsx
import { Menu } from "@/components/ouro/menu/menu";
```

## Example

```tsx
<Menu trigger={<Button variant="outline">Actions</Button>}>
  <MenuItem onClick={edit} shortcut={["mod", "e"]}>Edit</MenuItem>
  <MenuItem href="/recipes/1/print">Print</MenuItem>
  <MenuSeparator />
  <MenuItem tone="destructive" onClick={remove}>Delete</MenuItem>
</Menu>
<Menu contextTrigger={<RecipeCard recipe={recipe} />} items={[{ label: "Copy link", onClick: copy }]} />
```

## Usage

Pass `trigger` for a dropdown, `contextTrigger` for a context menu, or `label` for a menu in a `MenuBar`. Give the entries as children or as `items` data. To give the trigger a tooltip too, put the Tooltip inside `trigger`: `trigger={<Tooltip content="More actions"><Button aria-label="More">…</Button></Tooltip>}`.

It stacks above the page at `--ouro-layer`, a z-index of 50 by default (from ouro-base.css), like Dialog and Sheet.

## Use instead when

- Use `Select` instead to pick a value for a form, `Popover` for a panel with fields or text, and `Command` for a searchable list of every action in the app.

## Props

The props the modes share. Type: `MenuProps`.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` | The entries as `MenuItem`, `MenuCheckboxItem`, `MenuRadioGroup`, `MenuGroup`, `MenuSub` and `MenuSeparator`. |
| `className` | `string` | Layout only, applied to the menu panel: width (`w-64`; at least 12rem by default) and margins. The phone sheet keeps its full width. Appearance classes warn in development. |
| `closeLabel` | `string` | The text of the item that ends the phone sheet and closes it, for screen-reader and switch users, who cannot swipe or tap the backdrop. Default "Close". |
| `items` | `readonly MenuEntry[]` | The entries as data, in place of children: `items={[{ label: "Edit", onClick: edit }, { type: "separator" }]}`. |
| `onOpenChange` | `((open: boolean) => void)` | Called when the menu opens or closes. |
| `open` | `boolean` | Whether the menu is open, to control it; pass `onOpenChange` with it. |

## Modes

`MenuProps` is a union: pass the props of exactly one mode. Mixing them is a type error.

### Bar menu: `MenuBarMenuProps`

A menu in a `MenuBar`: `label` is its button's text.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | How the menu lines up with the trigger along that side. |
| `label` (required) | `string` |  | The text of the menu's button in the `MenuBar`: `<Menu label="File">`. |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | The side of the trigger the menu sits on; it flips when there is no room. |

Not in this mode (typed `never`):

- `contextTrigger`: Not with `label`. A menu in a `MenuBar` gets its button from `label`.
- `modal`: Not with `label`: the `MenuBar` decides how its menus hold the page.
- `trigger`: Not with `label`. A menu in a `MenuBar` gets its button from `label`.

### Context: `MenuContextProps`

A context menu: right-click or long-press on `contextTrigger` opens it at the pointer.

| Prop | Type | Description |
| --- | --- | --- |
| `contextTrigger` (required) | `ReactElement` | The area that opens the menu on right-click, or on a long press on touch screens: `contextTrigger={<div>…</div>}`. Its text stays selectable; only while a finger is down does it turn off text selection and the iOS callout, so a long press opens the menu. Give the same actions a visible control too: a context menu is hard to discover. |

Not in this mode (typed `never`):

- `align`: Not with `contextTrigger`: a context menu opens at the pointer.
- `label`: Not with `contextTrigger`. `label` is for a Menu inside `MenuBar`.
- `modal`: Not with `contextTrigger`: a context menu holds the page while it is open.
- `side`: Not with `contextTrigger`: a context menu opens at the pointer.
- `trigger`: Not with `contextTrigger`. Use one way to open a menu.

### Dropdown: `MenuDropdownProps`

A dropdown: `trigger` opens it. Below 640px it is a bottom sheet.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | How the menu lines up with the trigger along that side. |
| `modal` | `boolean` | `true` | Whether the open dropdown holds the page: it locks the page's scroll, and a click outside only closes it. `modal={false}` leaves the page scrolling and its controls live, and spares the browser a layout and repaint of the whole page each time the menu opens and closes: for a small menu that a list repeats on every row. The phone sheet always holds the page. |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | The side of the trigger the menu sits on; it flips when there is no room. |
| `trigger` (required) | `ReactElement` |  | The button that opens the menu, usually a Button: `trigger={<Button>Actions</Button>}`. |

Not in this mode (typed `never`):

- `contextTrigger`: Not with `trigger`. Use one way to open a menu.
- `label`: Not with `trigger`. `label` is for a Menu inside `MenuBar`.

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `asChild`: Not a prop. The trigger element is the button: `trigger={<Button>Actions</Button>}`.
- `isOpen`: Not a prop. Ouro UI uses `open` with `onOpenChange`: `<Menu open={open} onOpenChange={setOpen} trigger={…}>`.
- `onClose`: Not a prop. `onOpenChange(false)` fires on close: `<Menu open={open} onOpenChange={setOpen} trigger={…}>`.
- `options`: Not a prop. The entries are `items` (or children): `items={[{ label: "Edit", onClick: edit }]}`.
- `placement`: Not a prop. Ouro UI splits placement into `side` and `align`: `<Menu side="bottom" align="start">`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`). Your classes always win over Ouro UI's: Menu merges `className` last with `cn` (tailwind-merge), so a class that conflicts with its own replaces it.[^layout-classes]

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: use the `align` prop.

## Types

### `MenuCheckboxEntry`

A menu entry for `items`: an on/off setting that stays in the menu when toggled.

```ts
interface MenuCheckboxEntry {
  type: "checkbox";
  label: string;
  checked?: boolean | undefined;
  defaultChecked?: boolean | undefined;
  onCheckedChange?: ((checked: boolean) => void) | undefined;
  shortcut?: MenuShortcut | undefined;
  disabled?: boolean | undefined;
}
```

### `MenuEntry`

One entry in a Menu's `items`: an item (the default `type`), a checkbox, a radio group, a group, a submenu or a separator.

```ts
type MenuEntry =
  | MenuItemEntry
  | MenuCheckboxEntry
  | MenuRadioEntry
  | MenuGroupEntry
  | MenuSubmenuEntry
  | { type: "separator" };
```

### `MenuGroupEntry`

A menu entry for `items`: entries under a label.

```ts
interface MenuGroupEntry {
  type: "group";
  label: string;
  items: readonly MenuEntry[];
}
```

### `MenuItemEntry`

A menu entry for `items`: an action, or a link with `href`.

```ts
interface MenuItemEntry {
  type?: "item" | undefined;
  /** The item's text. */
  label: string;
  /** Runs when the item is chosen, by click, tap or Enter. The menu then closes. */
  onClick?: (() => void) | undefined;
  /** Makes the item a link, through `OuroProvider`'s router link for an internal path. */
  href?: string | undefined;
  /** The keys of the item's shortcut, shown at its end: `["mod", "e"]`. It does not bind them. */
  shortcut?: MenuShortcut | undefined;
  /** `"destructive"` for an action that deletes or cannot be undone. */
  tone?: "destructive" | undefined;
  /** Shown, but cannot be chosen. */
  disabled?: boolean | undefined;
}
```

### `MenuRadioEntry`

A menu entry for `items`: one choice of several, as radio items under an optional label.

```ts
interface MenuRadioEntry {
  type: "radio";
  /** Names the group: "Sort by". */
  label?: string | undefined;
  value?: string | undefined;
  defaultValue?: string | undefined;
  onValueChange?: ((value: string) => void) | undefined;
  options: ReadonlyArray<{ value: string; label: string; shortcut?: MenuShortcut | undefined; disabled?: boolean | undefined }>;
}
```

### `MenuShortcut`

Keys for a shortcut, as `Kbd` takes them: `["mod", "e"]` shows ⌘E on Apple platforms and Ctrl+E elsewhere.

```ts
type MenuShortcut = readonly string[] | (string & { 'Use an array: shortcut={["mod", "k"]}': never });
```

### `MenuSubmenuEntry`

A menu entry for `items`: an item that opens a nested menu.

```ts
interface MenuSubmenuEntry {
  type: "submenu";
  label: string;
  items: readonly MenuEntry[];
  disabled?: boolean | undefined;
  /** The text of the item that goes back from the submenu in the phone sheet. Default "Back". */
  backLabel?: string | undefined;
}
```

[^layout-classes]: test:registry/ouro/components/ouro/menu/menu.test.tsx::Menu dropdown > is at least 12rem wide, and className sets layout verify:testExists(registry/ouro/components/ouro/menu/menu.test.tsx::Menu dropdown > is at least 12rem wide, and className sets layout)
