Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

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.

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

PropTypeDefaultDescription
openbooleanWhether the menu is open. Pair with onOpenChange to control it.
defaultOpenbooleanfalseWhether 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".
childrenReactNodeThe trigger and the content: Menu.Trigger and Menu.Content.

Menu.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">.

Menu.Content

PropTypeDefaultDescription
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

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

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

Menu.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">.

Menu.Link

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

required

ReactNodeWhere it goes. Long labels truncate.

It also takes Omit<ComponentPropsWithRef<"a">, "children" | "onSelect">.

Menu.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">.

Menu.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">.

Menu.Separator

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

Menu.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.

Menu.SubContent

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

Menu.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">.

Menu.Trigger

It also takes ComponentPropsWithRef<typeof DropdownMenu.Trigger>.

Also exported

ExportDescription
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.

KeysWhat they do
Enter, Space or ArrowDownOpens it from the trigger, with focus on the first item
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 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.

See it in the roastery demo's customers and orders. See it in the Halfmoon shop demo. See it in the Deckhand demo.