Menu
A list of actions that opens from a trigger, on the floating surface in the dropdown layer.
import { Menu } from "@butteryui/core";Demo
When to use it
Use it for
- Actions that don't fit, or don't need to show, next to their control: duplicate, rename, delete
- Picking one of a few settings that apply at once, with Menu.RadioGroup, or turning one on or off with Menu.CheckboxItem
- A few pages to go to, such as an account menu's, with Menu.Link
Not for
- Choosing a value a form submits or a control shows: use Select
- A small form or anything with inputs: use Popover
- A site's main navigation: use NavBar, or Sidebar in an app
Examples
Price lists
Wholesale price lists, each with an actions menu at the end of its row that duplicates or deletes it.
3 price lists
Café prices
Office prices
Holiday prices
Saved card actions
A saved payment card's less common actions behind one trigger, with the destructive one set apart in danger.
Nothing chosen yet
Sort order
A sort order kept in state and picked from a radio group, applied to the list as soon as it's chosen.
Nyeri
Cerrado
Huila
Variants and states
Actions
Lined up with the trigger's end, 8px below it. It grows in from the trigger and fades out.
Submenus
An item that opens a second list beside it, once the pointer rests on it for 40ms or with ArrowRight.
Instant, as app chrome
entrance="instant" shows the menu complete in the next frame and removes it at once.
Groups and choices
A heading names each group; in a radio group the chosen item shows a check, and so does a checkbox item while it's on.
Props
Menu
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | Whether the menu 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 menu opens or closes. | |
entrance | "grow" | "instant" | "grow" from the trigger 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 trigger and the content: Menu.Trigger and Menu.Content. |
Menu.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">.
Menu.Content
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "end" | Which edge of the trigger it lines up with. Defaults to "end". |
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.Content>, "sideOffset" | "collisionPadding" | "forceMount" | "asChild">.
Menu.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">.
Menu.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">.
Menu.Link
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ComponentType<IconComponentProps> | An icon before the label. | |
childrenrequired | ReactNode | Where it goes. Long labels truncate. |
It also takes Omit<ComponentPropsWithRef<"a">, "children" | "onSelect">.
Menu.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">.
Menu.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">.
Menu.Separator
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.Separator>, "asChild">.
Menu.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. |
Menu.SubContent
It also takes Omit<ComponentPropsWithRef<typeof DropdownMenu.SubContent>, "sideOffset" | "alignOffset" | "collisionPadding" | "forceMount" | "asChild">.
Menu.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">.
Menu.Trigger
It also takes ComponentPropsWithRef<typeof DropdownMenu.Trigger>.
Also exported
| Export | Description |
|---|---|
MenuPrimitivesContext: Context<MenuPrimitives> | Which Radix menu the parts render through, DropdownMenu unless set. A menu built on another Radix menu,
as ContextMenu is, sets it around its content so the same parts work in it. |
MenuShortcutsContext: Context<MenuShortcutRegistry | null> | Set around a closed menu's parts, which then draw nothing and hand their items' shortcuts to it: how a
ContextMenu keeps its items' shortcuts working in its trigger while the menu isn't open. |
Accessibility
The trigger gets aria-haspopup and aria-expanded; the content is a menu of menuitems (menuitemcheckbox for Menu.CheckboxItem and menuitemradio in a radio group, each with aria-checked). Arrow keys move, typing jumps to an item, Enter or Space chooses, Escape closes. Focus goes back to the trigger as it closes, unless a press elsewhere closed it, and it never takes focus back once you've moved on. Groups are named by their headings. An item's shortcut is announced through aria-keyshortcuts, and a dot adds ", new" to its name. A submenu's item has aria-haspopup and aria-expanded; ArrowRight opens it and ArrowLeft closes it.
| Keys | What they do |
|---|---|
| Enter, Space or ArrowDown | Opens it from the trigger, with focus on the first item |
| 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 on the trigger |
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 the trigger, and so does a submenu; with entrance="instant" it shows and goes at once either way.
Layout
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, opens 8px from its trigger, stays 8px inside the window and scrolls inside when it's taller than the room there.
Related
See it in the roastery demo's customers and orders. See it in the Halfmoon shop demo. See it in the Deckhand demo.