Gesso
Actions

Context menu

Right-click menu on a record row. Open, copy id, change status, assign.

Context menu puts actions on the thing they act on. Right-click a row, or press and hold it on a touch screen, and the menu opens at the pointer. Its rows are the same as Dropdown menu's, with the check on the right and the bg-fill wash, and an item takes variant="destructive" for an action that deletes.

Right-click or long-press this row

Usage

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from '@/components/ui/context-menu';

<ContextMenu>
  <ContextMenuTrigger asChild>
    <div className="rounded-lg border border-border p-4">GES-184</div>
  </ContextMenuTrigger>
  <ContextMenuContent aria-label="Issue GES-184">
    <ContextMenuItem onSelect={openIssue}>Open</ContextMenuItem>
    <ContextMenuItem onSelect={copyId}>Copy id</ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem variant="destructive" onSelect={deleteIssue}>
      Delete
    </ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

The panel sits on bg-surface-raised with a thin ring-border-strong edge, in font-sans, and fades in and out without sliding, as Dropdown menu does. An icon placed in a row is drawn at 16px with a gap before the label, and inset lines up a row without one.

Bind it to the thing it acts on

ContextMenuTrigger wraps its children in a <span>, or with asChild hands its behavior to its one child, so a table row or a card can be the trigger itself. Controls inside it keep their own clicks, and a right click anywhere in it, on those controls too, opens its menu. Pass disabled to the trigger and a right click shows the browser's own menu instead.

Where it opens

The menu opens with its top-left corner just right of the pointer, or of the finger on a touch screen, and flips to stay on screen near an edge. A press with a finger or pen opens it after 700ms, and moving first cancels it, so scrolling past a row does not open the menu. The trigger also turns off the callout iOS shows on a long press. A menu taller than the room left on screen stops at the edge and scrolls.

open and onOpenChange work as on Radix ContextMenu, but the menu is placed where the last right click or press was, so opening it from code before either puts it in the top-left corner of the window.

Checked rows close the menu

Checkbox and radio rows mark the current value with a check on the right, as in Dropdown menu. Here a checkbox row closes the menu when toggled, like any other row. To keep the menu open for another pick, call event.preventDefault() in the row's onSelect, as Pinned does below.

<ContextMenuCheckboxItem
  checked={pinned}
  onCheckedChange={setPinned}
  onSelect={(event) => event.preventDefault()}
>
  Pinned
</ContextMenuCheckboxItem>
Right-click or long-press this card

Destructive rows

variant="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 separator, and ask with Confirm first when the action cannot be undone.

Props

Every part takes the props of its Radix part, as on Radix ContextMenu, plus these.

PropTypeDefaultWhat it does
variant"default" | "destructive""default"On ContextMenuItem. destructive paints the row in the destructive ink with a destructive tint on focus.
insetbooleanOn ContextMenuItem, ContextMenuSubTrigger, and ContextMenuLabel. Pads the left edge so the text starts where an icon row's label does.

Accessibility

  • The trigger is a <span> that does not take focus and shows no sign of the menu, which opens on right click or a 700ms press with a finger or pen, so keyboard and screen reader users may never learn it is there. Put every action in it somewhere they can reach too, like a Menu behind a button on the row.
  • From the keyboard it opens only when focus is on or inside the trigger and the browser sends a context menu event, as the context menu key does.
  • The menu has no name of its own, so give ContextMenuContent an aria-label that says what it acts on, like aria-label="Issue GES-184".
  • In the menu the keys are Dropdown menu's. ↑ and ↓ move between rows and stop at the ends, Home and End jump to the first and last, typing a letter moves to the next row that starts with it, and Enter or Space picks the focused row. → opens a submenu and ← closes it.
  • Escape closes the menu, from a submenu too, and focus goes back to where it was before the menu opened.
  • Rows are menuitem, checkbox rows are menuitemcheckbox and radio rows are menuitemradio, each with aria-checked. ContextMenuLabel is plain text that the arrow keys skip.
  • variant="destructive" changes only the color, so let the label say what the action does, 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 to open the same rows from a button, which every user can find, including on a keyboard. It has the same checkbox and radio rows.
  • Menu to open them from a button when every row is an action with an optional icon, shortcut, or destructive style. A row whose actions matter often has both, a context menu and a button that opens a Menu with the same actions.

Install

shadcn add @gesso/context-menu

On this page