# Menu
A dropdown menu component that displays a list of actionable items when triggered. Supports icons, descriptions, selected state, locked items, and disabled items.
```tsx demo
import { Button } from "@tenstorrent/vesper/button";
import { Menu } from "@tenstorrent/vesper/menu";
export default function MenuDemo() {
return (
);
}
```
> [!IMPORTANT]
>
> A `Menu`'s children must be a single React element that can receive a ref and event handlers, since that element becomes the menu's trigger. Passing a fragment, plain text, or multiple elements renders the children as-is, without a menu attached to them.
## Options
| Prop | Type | Description | Default |
| -------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------- |
| `items` | `MenuItemProps[]` | The list of menu items rendered in the dropdown, in the order they are provided. | — |
| `children` | `ReactElement` | The trigger element that opens the menu. | — |
| `width` | `number` | The width of the menu dropdown in pixels. Scales with the base rem size. | `200` |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | The preferred side of the trigger to render the menu against. | `"bottom"` |
| `sideOffset` | `number` | The distance in pixels from the trigger to the menu. Scales with the base rem size. | `8` |
| `align` | `"start" \| "center" \| "end"` | The alignment of the menu relative to the trigger along the perpendicular axis. | `"start"` |
| `alignOffset` | `number` | An offset in pixels from the aligned edge of the trigger. Scales with the base rem size. | `0` |
| `open` | `boolean` | Controls the open state of the menu (controlled mode). | — |
| `defaultOpen` | `boolean` | Whether the menu is open by default (uncontrolled mode). | `false` |
| `onOpenChange` | `(open: boolean) => void` | Callback fired when the open state changes. | — |
| `container` | `HTMLElement \| ShadowRoot \| null \| RefObject` | Specify the element or shadow root to portal the menu into. | — |
| `anchor` | `HTMLElement \| null \| RefObject` | Specify the element to position the menu against. Default behavior anchors the menu to the trigger element. | — |
### `MenuItemProps` options
| Property | Type | Description | Default |
| ------------- | --------------------------------------------------------------- | ----------------------------------------------------------------- | ----------- |
| `text` | `string` | The text label displayed for the menu item. | — |
| `description` | `string` | An optional secondary description displayed below the text label. | — |
| `icon` | `ReactNode` | An optional icon element rendered to the left of the text label. | — |
| `style` | `"default" \| "danger" \| "locked" \| "selected" \| "disabled"` | The visual and behavioral style of the menu item. | `"default"` |
| `onSelect` | `() => void` | Callback fired when the menu item is selected. **Required.** | — |
## Examples
### Basic usage
Render a `Menu` by passing it an array of `items` and a single element to use as its trigger. Every item needs `text` to display and an `onSelect` callback to run when it is chosen:
```tsx demo
import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Menu } from "@tenstorrent/vesper/menu";
import { Typography } from "@tenstorrent/vesper/typography";
export default function BasicMenu() {
const [selected, setSelected] = useState(null);
return (
Last selected item: {selected ?? "none"}
);
}
```
Clicking the trigger opens the menu, and choosing an item fires its `onSelect` callback function and closes the menu. Any element that can be focused and receive a ref works as a trigger.
### Describing items
Each item can render an `icon` to the left of its label, and a `description` underneath it for the items whose outcome is not obvious from their label alone. Descriptions are optional, and can be mixed with items that only have a label:
```tsx demo
import { IconButton } from "@tenstorrent/vesper/icon-button";
import { Copy, Download, Ellipses, Gear } from "@tenstorrent/vesper/icons";
import { Menu } from "@tenstorrent/vesper/menu";
export default function DescriptiveMenu() {
return (
,
description: "Anyone with the link can view",
onSelect: () => {},
},
{
text: "Download",
icon: ,
description: "Exports the current view as a CSV",
onSelect: () => {},
},
{
text: "Settings",
icon: ,
onSelect: () => {},
},
]}
>
}
/>
);
}
```
### Item styles
Each item can be rendered in one of five styles via its `style` prop:
| Style | When to use |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `"default"` | Default style. Use for regular actions. |
| `"selected"` | Use when the item represents the currently active option out of a set. Renders a checkmark at the end of the item. |
| `"danger"` | Use for destructive or potentially irreversible actions. Renders the item in the error color scheme. |
| `"locked"` | Use for actions the user could take, but currently cannot, eg. because of their permissions. Renders a lock icon and prevents interaction. |
| `"disabled"` | Use for actions that do not apply in the current context, eg. pasting with an empty clipboard. Dims the item and prevents interaction. |
```tsx demo
import { Button } from "@tenstorrent/vesper/button";
import { Blackhole, Globe, Tenstorrent } from "@tenstorrent/vesper/icons";
import { Menu } from "@tenstorrent/vesper/menu";
export default function MenuItemStyles() {
return (
, onSelect: () => {} },
{
text: "Selected item",
icon: ,
style: "selected",
onSelect: () => {},
},
{
text: "Danger item",
icon: ,
style: "danger",
onSelect: () => {},
},
{ text: "Locked item", style: "locked", onSelect: () => {} },
{ text: "Disabled item", style: "disabled", onSelect: () => {} },
]}
>
);
}
```
> [!NOTE]
>
> `"locked"` and `"disabled"` items cannot be activated: their `onSelect` never fires, and the menu stays open when one of them is clicked. They remain reachable with the keyboard, so assistive technology can still announce them and their state.
### Setting the width
A menu is `200px` wide by default. Pass a `number` to the `width` prop to render it with a different width. The value passed scales with the base rem size:
```tsx demo
import { Button } from "@tenstorrent/vesper/button";
import { Menu } from "@tenstorrent/vesper/menu";
const ITEMS = [
{ text: "Rename", onSelect: () => {} },
{ text: "Duplicate", onSelect: () => {} },
];
export default function MenuWidths() {
return (
);
}
```
### Positioning the menu
By default, a `Menu` opens below its trigger and is aligned to its starting edge. You can adjust this via the `side`, `sideOffset`, `align`, and `alignOffset` props.
The `side` prop determines which trigger edge the menu sits against. Possible values are `"top"`, `"bottom"`, `"left"`, and `"right"`. The default value is `"bottom"`.
The `sideOffset` prop determines how far away in pixels the menu is from its trigger's edge. The default value is `8`. This value scales with the base rem size.
The `align` prop determines which end of the `side` to align the menu to. Possible values are `"start"`, `"center"`, and `"end"`. The default value is `"start"`.
The `alignOffset` prop adjusts how many pixels the menu is offset from its alignment. The default value is `0`. This value scales with the base rem size.
These props express a preference rather than a guarantee: a menu that would otherwise overflow the viewport is flipped and shifted to stay on screen.
The demo below shows how the `side` and `align` props interact with each other:
```tsx demo
import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Menu, type MenuProps } from "@tenstorrent/vesper/menu";
import { Select } from "@tenstorrent/vesper/select";
type MenuSide = NonNullable;
type MenuAlign = NonNullable;
const SIDES = ["top", "bottom", "left", "right"] as const;
const ALIGNMENTS = ["start", "center", "end"] as const;
export default function MenuPositionPlayground() {
const [side, setSide] = useState("bottom");
const [align, setAlign] = useState("start");
return (
);
}
```
### Controlling the open state
By default, a `Menu` tracks its own open state. Pass `defaultOpen` when a menu should already be open on first render, while still letting it manage itself afterwards:
```tsx
import { Button } from "@tenstorrent/vesper/button";
import { Menu } from "@tenstorrent/vesper/menu";
export default function DefaultOpenMenu() {
return (
);
}
```
If you need to own the open state, use the `open` and `onOpenChange` props together. `onOpenChange` fires with the menu's next open state value:
```tsx demo
import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Menu } from "@tenstorrent/vesper/menu";
import { Typography } from "@tenstorrent/vesper/typography";
export default function ControlledMenu() {
const [open, setOpen] = useState(false);
return (
Open state: {String(open)}
);
}
```
### Anchoring the menu elsewhere
A menu is positioned against its trigger by default. Pass the `anchor` prop an element or an element ref to position it against something else. The menu below is aligned to the end of the card rather than its trigger:
```tsx demo
import { useRef } from "react";
import { IconButton } from "@tenstorrent/vesper/icon-button";
import { Ellipses } from "@tenstorrent/vesper/icons";
import { Material } from "@tenstorrent/vesper/material";
import { Menu } from "@tenstorrent/vesper/menu";
import { Typography } from "@tenstorrent/vesper/typography";
export default function AnchoredMenu() {
const cardRef = useRef(null);
return (
Inference cluster
);
}
```
### Rendering the menu in another container
A menu renders in a portal, attached to `document.body`, so it is never clipped by the overflow of the elements it sits in. Pass the `container` prop an element, a shadow root, or a ref to either when it needs to be portalled somewhere else:
```tsx
import { useRef } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Menu } from "@tenstorrent/vesper/menu";
export default function PortalledMenu() {
const containerRef = useRef(null);
return (
);
}
```
> [!NOTE]
>
> A `Menu` rendered inside a `