Gesso
Actions

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.

PropTypeDefaultWhat it does
iconComponentType<{ className?: string }>An icon component drawn at 16px before the label. MenuSubTrigger takes it too.
shortcutstringText at the end of the row, like "⌘E". It does not bind the key.
destructivebooleanPaints 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.
  • destructive changes only the color, so nothing tells a screen reader the action is destructive. Let the label say it, like "Delete issue".
  • A disabled row sets aria-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

On this page