Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

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

Fall wholesale pitch

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.

Café price list

Right-click it, or focus it and press Shift+F10

An item that opens a second list beside it, once the pointer rests on it for 40ms or with ArrowRight.

Slide 3: The coffees on offer

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.

Monday roasts

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.

Archived price list

Right-click it, or focus it and press Shift+F10

Shared price list

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.

Order 1042

Right-click it, or focus it and press Shift+F10

Props

ContextMenu

PropTypeDefaultDescription
openbooleanWhether 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.
defaultOpenbooleanfalseWhether 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".
childrenReactNodeThe region and the menu: ContextMenu.Trigger and ContextMenu.Content.

ContextMenu.CheckboxItem

PropTypeDefaultDescription
checkedbooleanWhether it's on. Pair with onCheckedChange to control it.
defaultCheckedbooleanfalseWhether 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.
iconComponentType<IconComponentProps>An icon before the label.
shortcutShortcutIts 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.
children

required

ReactNodeWhat 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

PropTypeDefaultDescription
labelstringA heading for the group, read as its name.

It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.Group>, "asChild">.

ContextMenu.Item

PropTypeDefaultDescription
iconComponentType<IconComponentProps>An icon before the label, such as Copy from lucide-react.
mediaReactNodeA small element before the label in place of an icon, such as a 20px Avatar or InitialsTile.
shortcutShortcutIts 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.
dotbooleanDraws an accent dot at the item's end for something new behind it; its name says so too.
dotLabelstring"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".
children

required

ReactNodeWhat the action is. Long labels truncate.

It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.Item>, "asChild">.

ContextMenu.RadioGroup

PropTypeDefaultDescription
valuestringThe chosen item's value. Pair with onValueChange to control it.
defaultValuestringThe 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.
labelstringA heading for the group, read as its name.

It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.RadioGroup>, "asChild" | "value" | "onValueChange">.

ContextMenu.RadioItem

PropTypeDefaultDescription
iconComponentType<IconComponentProps>An icon before the label.
mediaReactNodeA small element before the label in place of an icon, such as a 20px InitialsTile.
children

required

ReactNodeWhat 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

PropTypeDefaultDescription
openbooleanWhether the submenu is open. Pair with onOpenChange to control it.
defaultOpenbooleanfalseWhether it starts open when uncontrolled. Defaults to false.
onOpenChange((open: boolean) => void)Called when the submenu opens or closes.
childrenReactNodeMenu.SubTrigger and Menu.SubContent.

ContextMenu.SubContent

It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.SubContent>, "sideOffset" | "alignOffset" | "collisionPadding" | "forceMount" | "asChild">.

ContextMenu.SubTrigger

PropTypeDefaultDescription
iconComponentType<IconComponentProps>An icon before the label.
children

required

ReactNodeWhat the submenu holds, such as "Switch workspace". Long labels truncate.

It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.SubTrigger>, "asChild">.

ContextMenu.Trigger

PropTypeDefaultDescription
asChildbooleanMakes 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.
disabledbooleanfalseLeaves 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.

KeysWhat they do
Shift+F10 or the context-menu keyOpens the menu at the focused element, with focus on its first item
An item's shortcutRuns the item while focus is in the trigger and the menu is closed
ArrowDown or ArrowUpMoves to the next or previous item, skipping disabled ones
Home or EndMoves to the first or last item
Typing lettersMoves to the item whose label starts with them
Enter or SpaceChooses the item with focus and closes the menu; on a submenu's item, opens the submenu
ArrowRightOpens a submenu from its item, with focus on its first item
ArrowLeftCloses the submenu and goes back to its item
EscapeCloses 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.

See it in the Deckhand demo.