# 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 ( {} }, { text: "Duplicate", onSelect: () => {} }, { text: "Delete", style: "danger", onSelect: () => {} }, ]} > ); } ``` > [!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 (
setSelected("Rename") }, { text: "Duplicate", onSelect: () => setSelected("Duplicate") }, { text: "Delete", style: "danger", onSelect: () => setSelected("Delete"), }, ]} > 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 (
setAlign(align as MenuAlign)} options={[...ALIGNMENTS]} style={{ width: "var(--vesper-spacing-28)" }} />
{} }, { text: "Duplicate", onSelect: () => {} }, ]} >
); } ``` ### 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 ( {} }, { text: "Duplicate", onSelect: () => {} }, ]} > ); } ``` 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 (
{} }, { text: "Duplicate", onSelect: () => {} }, ]} >
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 {} }, { text: "View logs", onSelect: () => {} }, { text: "Delete", style: "danger", onSelect: () => {} }, ]} > } /> ); } ``` ### 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 (
{} }, { text: "Duplicate", onSelect: () => {} }, ]} >
); } ``` > [!NOTE] > > A `Menu` rendered inside a `` (which [Modal](./modal.mdx) and [Sheet](./sheet.mdx) both use) is portalled into that dialog automatically, so its items stay above the dialog's content. Passing `container` explicitly always takes precedence over this behavior.