ContextMenu
A menu of actions for what's under the pointer: it opens where a right-click or a long press lands, or at the focused element from the keyboard.
import { ContextMenu } from "@butteryui/react";Demo
Right-click it, or focus it and press Shift+F10
When to use it
Use it for
- Actions on something people point at in a list, a board, a canvas or a table: duplicate, move, recolor, delete
- The actions a toolbar or a row's … menu already offers, for people who reach for the right mouse button
- Shortcuts worth learning: an item shows its keys, and the keys work while focus is in the region
Not for
- The only way to an action: many people never right-click, so put it on a button, a … menu or the toolbar too
- A list of actions that opens from a button: use Menu
- Choosing a value a form submits: use Select
Examples
Deck files
A folder of decks: right-click one, or focus it and press Shift+F10, to open, duplicate, move or delete it; its shortcuts work on the focused deck.
3 decks · No deck open
Order rows
Wholesale orders as table rows, each with its own menu: whether it's paid is kept in state, shown on the row and flipped from the menu or with P.
Right-click an order, or focus it and press Shift+F10
Roast board
A roast day board with menus inside menus: right-click the board to add a batch or hide the weights, or a batch to set its status, pin it or delete it.
Monday's roasts · 3 batches
Variants and states
Right-click, a long press or Shift+F10
The menu's corner opens beside the pointer, or beside the middle of the focused card, and it grows in from there. Items show their icons and shortcuts, and Delete is drawn in danger.
Right-click it, or focus it and press Shift+F10
Submenus
An item that opens a second list beside it, once the pointer rests on it for 40ms or with ArrowRight.
Right-click it, or focus it and press Shift+F10
Checkbox and radio items
A checkbox item shows a check while it's on, and its shortcut flips it from the card. In a radio group the chosen item shows one, under a heading that names the group.
Sorted by roast day, weights shown
Disabled items and a disabled card
A disabled item stays in the list and the arrow keys skip it. A disabled trigger leaves the right-click to the browser's own menu.
Right-click it, or focus it and press Shift+F10
Right-click shows the browser's own menu
Instant, as app chrome
entrance="instant" shows the menu complete in the next frame and removes it at once.
Right-click it, or focus it and press Shift+F10
Props
ContextMenu
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | Whether the menu is open. Pair with onOpenChange to control it. Opened this way rather than by a press,
it opens at the trigger, as the keyboard opens it. | |
defaultOpen | boolean | false | Whether it starts open, at the trigger, when uncontrolled. Defaults to false. |
onOpenChange | ((open: boolean) => void) | Called when the menu opens or closes. | |
entrance | "grow" | "instant" | "grow" from where it opened with a fade, or "instant", complete in the next frame, as app chrome moves.
Defaults to what's set around it ("instant" inside AppShell layout="sidebar"), else "grow". | |
children | ReactNode | The region and the menu: ContextMenu.Trigger and ContextMenu.Content. |
ContextMenu.CheckboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | Whether it's on. Pair with onCheckedChange to control it. | |
defaultChecked | boolean | false | Whether it starts on when uncontrolled. Defaults to false. An uncontrolled item starts from it each time
its menu opens, so keep checked in state to remember a choice. |
onCheckedChange | ((checked: boolean) => void) | Called with whether it's on once it's chosen. | |
icon | ComponentType<IconComponentProps> | An icon before the label. | |
shortcut | Shortcut | Its keyboard shortcut, shown at its end and announced through aria-keyshortcuts. In a ContextMenu the
shortcut also turns it on or off while focus is in the trigger and the menu is closed, which needs
checked; in a Menu, bind it with useShortcut. | |
childrenrequired | ReactNode | What it turns on or off, such as "Show the grid". Long labels truncate. |
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.CheckboxItem>, "asChild" | "checked" | "onCheckedChange">.
ContextMenu.Content
It also takes Omit<ComponentPropsWithRef<typeof RadixContextMenu.Content>, "side" | "sideOffset" | "align" | "alignOffset" | "collisionPadding" | "forceMount" | "asChild">.
ContextMenu.Group
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | A heading for the group, read as its name. |
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.Group>, "asChild">.
ContextMenu.Item
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ComponentType<IconComponentProps> | An icon before the label, such as Copy from lucide-react. | |
media | ReactNode | A small element before the label in place of an icon, such as a 20px Avatar or InitialsTile. | |
shortcut | Shortcut | Its keyboard shortcut, written as useShortcut takes it, such as "mod+d": shown at the item's end and
announced through aria-keyshortcuts. In a ContextMenu the shortcut also runs onSelect while focus is
in the trigger and the menu is closed; in a Menu, bind it with useShortcut. | |
dot | boolean | Draws an accent dot at the item's end for something new behind it; its name says so too. | |
dotLabel | string | "new" | What the dot adds to the item's name. Defaults to "new". |
tone | "neutral" | "danger" | "neutral" | "danger" draws it in danger, for an action that deletes or resets something. Defaults to "neutral". |
childrenrequired | ReactNode | What the action is. Long labels truncate. |
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.Item>, "asChild">.
ContextMenu.RadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | The chosen item's value. Pair with onValueChange to control it. | |
defaultValue | string | The value chosen at first when uncontrolled. An uncontrolled group starts from it each time its menu
opens, so keep value in state to remember a choice. | |
onValueChange | ((value: string) => void) | Called with the value of the item chosen. | |
label | string | A heading for the group, read as its name. |
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.RadioGroup>, "asChild" | "value" | "onValueChange">.
ContextMenu.RadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ComponentType<IconComponentProps> | An icon before the label. | |
media | ReactNode | A small element before the label in place of an icon, such as a 20px InitialsTile. | |
childrenrequired | ReactNode | What the choice is. Long labels truncate. |
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.RadioItem>, "asChild">.
ContextMenu.Separator
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.Separator>, "asChild">.
ContextMenu.Sub
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | Whether the submenu is open. Pair with onOpenChange to control it. | |
defaultOpen | boolean | false | Whether it starts open when uncontrolled. Defaults to false. |
onOpenChange | ((open: boolean) => void) | Called when the submenu opens or closes. | |
children | ReactNode | Menu.SubTrigger and Menu.SubContent. |
ContextMenu.SubContent
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.SubContent>, "sideOffset" | "alignOffset" | "collisionPadding" | "forceMount" | "asChild">.
ContextMenu.SubTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ComponentType<IconComponentProps> | An icon before the label. | |
childrenrequired | ReactNode | What the submenu holds, such as "Switch workspace". Long labels truncate. |
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.SubTrigger>, "asChild">.
ContextMenu.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | Makes your own element the region, with the trigger's props merged onto it, such as a row, a card or a canvas. It has to take focus, or hold something that does, for the keyboard to open the menu. | |
disabled | boolean | false | Leaves the region to the browser's own menu: a right-click, a long press and the menu keys open nothing,
and the items' shortcuts don't run. Defaults to false. |
It also takes ComponentPropsWithRef<"div">.
Accessibility
The trigger keeps its own role and name, and has to take focus or hold something that does. The context-menu key or Shift+F10 opens the menu at the focused element (at the option a focused list's aria-activedescendant points at) with focus on its first item. The content is a menu of menuitems (menuitemcheckbox and menuitemradio, with aria-checked), named by its aria-label. Arrow keys move, typing jumps to an item, Enter or Space chooses, and Escape closes it and puts focus back where it was, unless a press elsewhere closed it. An item's shortcut is announced through aria-keyshortcuts and works while focus is in the trigger and the menu is closed. A submenu's item has aria-haspopup and aria-expanded; ArrowRight opens it and ArrowLeft closes it.
| Keys | What they do |
|---|---|
| Shift+F10 or the context-menu key | Opens the menu at the focused element, with focus on its first item |
| An item's shortcut | Runs the item while focus is in the trigger and the menu is closed |
| ArrowDown or ArrowUp | Moves to the next or previous item, skipping disabled ones |
| Home or End | Moves to the first or last item |
| Typing letters | Moves to the item whose label starts with them |
| Enter or Space | Chooses the item with focus and closes the menu; on a submenu's item, opens the submenu |
| ArrowRight | Opens a submenu from its item, with focus on its first item |
| ArrowLeft | Closes the submenu and goes back to its item |
| Escape | Closes the menu and puts focus back where it was |
Motion
It appears with a `Presence` preset and leaves faster than it came, in about 60% of the time.
With reduced motion: It fades in and out over 100ms without growing from its corner, and so does a submenu; with entrance="instant" it shows and goes at once either way.
Layout
Its corner opens 2px beside the pointer, or beside the middle of the focused element: to the right and level with it, to the left where the right has no room, and ending level with it where there's no room below. It keeps 8px inside the window and scrolls inside when it's taller than the room. Each item is --bt-h-button (36px) tall and its whole row is its hit area; long labels truncate with an ellipsis and show in full as their title. The list is at least --bt-w-menu-min (200px) wide. The trigger keeps its own size and layout.
Related
See it in the Deckhand demo.