Menu
Opinionated action menu. Icons, shortcuts, and a destructive item on DropdownMenu.
Menu is a list of one-shot actions on a thing, like edit, duplicate, and delete. It is
Dropdown menu with icon, shortcut, and
destructive props on its items, so you do not compose the icon, label, and shortcut in
every row by hand. The root, trigger, panel, groups, labels, separators, and submenus are
Dropdown menu's own, so the look, the motion, and the keyboard are the same.
Usage
import { PencilSimple, Trash } from '@phosphor-icons/react';
import { Button } from '@/components/ui/button';
import {
Menu,
MenuContent,
MenuItem,
MenuSeparator,
MenuTrigger,
} from '@/components/ui/menu';
<Menu>
<MenuTrigger asChild>
<Button variant="ghost" size="sm">
Actions
</Button>
</MenuTrigger>
<MenuContent>
<MenuItem icon={PencilSimple} shortcut="⌘E" onSelect={editIssue}>
Edit
</MenuItem>
<MenuSeparator />
<MenuItem icon={Trash} destructive onSelect={deleteIssue}>
Delete
</MenuItem>
</MenuContent>
</Menu>A row runs its action in onSelect, which fires on a click, Enter, or Space, and then the
menu closes.
Icons are components
icon takes an icon component rather than an element, and the row draws it at 16px before
the label. Any component that takes a className fits, so a Phosphor icon, an icon from
another set, or your own SVG component all work. MenuSubTrigger takes icon too. A row
without an icon starts its label at the left edge, so pass inset to line it up with the
rows that have one, as Active does in the Move to submenu above.
Because the prop is a component, set it from a client component, as with
Button's icons. A Server Component cannot pass a component
to a client one, so <MenuItem icon={Trash}> in a page file fails. Put the menu in a small
"use client" file instead.
Shortcuts are labels
shortcut sets a string like "⌘E" at the end of the row in small, faded text. It only
shows the keys and does not listen for them, so bind the shortcut yourself where the action
lives.
Destructive rows
destructive paints the row in the destructive ink, and its focus wash is a faint
destructive tint instead of bg-fill. Put it last, after a MenuSeparator, and ask with
Confirm first when the action cannot be undone.
Mixing in Dropdown menu rows
Menu has no checkbox or radio rows of its own. Its parts are Dropdown menu's, so a
DropdownMenuCheckboxItem or a DropdownMenuRadioGroup can sit inside a MenuContent and
behaves as it does there, with the check on the right and the menu staying open on toggle.
Props
MenuItem takes every prop of DropdownMenuItem, including inset, disabled, and
onSelect, plus these. The other parts are Dropdown menu's, as on Radix DropdownMenu.
| Prop | Type | Default | What it does |
|---|---|---|---|
icon | ComponentType<{ className?: string }> | An icon component drawn at 16px before the label. MenuSubTrigger takes it too. | |
shortcut | string | Text at the end of the row, like "⌘E". It does not bind the key. | |
destructive | boolean | Paints the row in the destructive ink, with a destructive tint on focus. |
Accessibility
- It is Dropdown menu underneath, so the keyboard is the same. Enter, Space, or ↓ on the trigger opens the menu and focuses the first row, ↑ and ↓ move between rows, Home and End jump to the ends, and typing a letter moves to the next row that starts with it. → opens a submenu and ← closes it.
- Escape closes the menu, from a submenu too, and focus goes back to the trigger.
- The open menu takes the trigger's label as its name, and rows are
menuitem. A row's name is its label and its shortcut text read together, and a Phosphor icon adds nothing to it. destructivechanges only the color, so nothing tells a screen reader the action is destructive. Let the label say it, like "Delete issue".- A
disabledrow setsaria-disabled, drops to 64% opacity, and is skipped by the arrow keys and typeahead. - While the menu is open the rest of the page is hidden from assistive tech, ignores the
pointer, and does not scroll, unless the root sets
modal={false}. - Rows are 32px tall and do not grow on a touch screen.
- Under reduced motion the panel opens and closes without the fade, and rows change color without a transition.
- The highlight is only a background wash, which forced colors repaints, so in Windows high contrast the focused row has no mark of its own.
When to use something else
- Dropdown menu for checkbox and radio rows, or a row you lay out yourself. Menu is that menu with icon, shortcut, and destructive props on its items, so the two mix.
- Context menu for the same rows opened by right click or a long press on the thing they act on, rather than from a button.
- Select to pick one value in a form, and Combobox when the list is long enough to search.
Install
shadcn add @gesso/menu