Menus and pickers
Menu
Show a list of commands and settings from a button.
"use client"
import {
Menu,
MenuContent,
MenuItem,
MenuItemIcon,
MenuSeparator,
MenuSub,
MenuSubContent,
MenuSubTrigger,
MenuTrigger,
} from "macbetter/menu"
import { ArchiveIcon, CopyIcon, FolderIcon, PencilIcon, PinIcon, ShareIcon, TrashIcon } from "lucide-react"
export default function Example() {
return (
<Menu onAction={(value) => console.log(value)}>
<MenuTrigger>Note</MenuTrigger>
<MenuContent>
<MenuItem value="rename" shortcut="mod+r">
<MenuItemIcon>
<PencilIcon />
</MenuItemIcon>
Rename
</MenuItem>
<MenuItem value="duplicate" shortcut="mod+d">
<MenuItemIcon>
<CopyIcon />
</MenuItemIcon>
Duplicate
</MenuItem>
<MenuItem value="pin" shortcut="mod+shift+p">
<MenuItemIcon>
<PinIcon />
</MenuItemIcon>
Pin Note
</MenuItem>
<MenuSub>
<MenuSubTrigger>
<MenuItemIcon>
<FolderIcon />
</MenuItemIcon>
Move To
</MenuSubTrigger>
<MenuSubContent>
<MenuItem value="move-personal">Personal</MenuItem>
<MenuItem value="move-work">Work</MenuItem>
<MenuItem value="move-recipes">Recipes</MenuItem>
</MenuSubContent>
</MenuSub>
<MenuSeparator />
<MenuItem value="share" shortcut="mod+alt+s">
<MenuItemIcon>
<ShareIcon />
</MenuItemIcon>
Share…
</MenuItem>
<MenuItem value="archive" disabled>
<MenuItemIcon>
<ArchiveIcon />
</MenuItemIcon>
Archive
</MenuItem>
<MenuSeparator />
<MenuItem value="delete" destructive shortcut="mod+backspace">
<MenuItemIcon>
<TrashIcon />
</MenuItemIcon>
Delete
</MenuItem>
</MenuContent>
</Menu>
)
}When to use it
Use a menu, or pull-down button, when a button stands for several related commands: what you can do to a note, how to sort a list, where to share a file. Every command in it should also be somewhere permanent, such as the menu bar on the Mac, because a menu hides what it holds until someone opens it.
- Keep menus short. Group related items with separators and put the most used ones first.
- Name items with verbs or nouns in title case, and end an item that asks for more input with an ellipsis (Share…).
- Put destructive items last, on their own, and mark them
destructive. - Show keyboard shortcuts on the Mac. People learn them from menus.
Usage
import { Menu, MenuContent, MenuItem, MenuTrigger } from "macbetter/menu"MenuTrigger is a macbetter button with a pull-down chevron, and takes the button's variant, tone, size and shape. Turn the chevron off with indicator={false} for an icon-only trigger, and name it with aria-label.
Icons, shortcuts and submenus
Put an icon in MenuItemIcon so screen readers and typeahead skip it. On the Mac icons sit before the label, and labels line up when only some items have one. On iOS and iPadOS they move to the trailing edge. shortcut="mod+shift+s" shows the keys, right-aligned in the secondary label color, and sets aria-keyshortcuts; your app still owns the key handler. MenuSub, MenuSubTrigger and MenuSubContent add another level, and the sub trigger draws its own chevron.
Checkboxes, radio groups and section labels
MenuCheckboxItem and MenuRadioItem render their own checkmark. The menu reserves a leading check column only when it has something to check, as the Mac does, so plain menus stay compact. MenuGroupLabel titles a group.
"use client"
import {
Menu,
MenuCheckboxItem,
MenuContent,
MenuGroupLabel,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuTrigger,
} from "macbetter/menu"
export default function Example() {
return (
<Menu>
<MenuTrigger>View</MenuTrigger>
<MenuContent>
<MenuRadioGroup name="sort" defaultValue="modified">
<MenuGroupLabel>Sort By</MenuGroupLabel>
<MenuRadioItem value="modified">Date Modified</MenuRadioItem>
<MenuRadioItem value="created">Date Created</MenuRadioItem>
<MenuRadioItem value="title">Title</MenuRadioItem>
</MenuRadioGroup>
<MenuSeparator />
<MenuCheckboxItem value="group-by-date" defaultChecked>
Group by Date
</MenuCheckboxItem>
<MenuCheckboxItem value="show-previews" defaultChecked>
Show Note Previews
</MenuCheckboxItem>
<MenuCheckboxItem value="show-attachments">Show Attachments</MenuCheckboxItem>
</MenuContent>
</Menu>
)
}Presentation
Menus open as a popover on every platform, which is how pull-down menus look on iOS too. presentation="sheet" slides the menu up from the bottom of the screen instead, and presentation="auto" does that only on small touch screens. Reach for it when a menu is long; a short action list reads better as a popover next to its button. In a sheet, a submenu replaces the list, and Escape or a swipe down goes back.
"use client"
import {
Menu,
MenuContent,
MenuItem,
MenuItemIcon,
MenuSeparator,
MenuSub,
MenuSubContent,
MenuSubTrigger,
MenuTrigger,
} from "macbetter/menu"
import {
CopyIcon,
FolderIcon,
LinkIcon,
MailIcon,
MessageCircleIcon,
PrinterIcon,
TrashIcon,
} from "lucide-react"
export default function Example() {
return (
<Menu presentation="auto" onAction={(value) => console.log(value)}>
<MenuTrigger>Share Photo</MenuTrigger>
<MenuContent>
<MenuItem value="copy">
<MenuItemIcon>
<CopyIcon />
</MenuItemIcon>
Copy
</MenuItem>
<MenuItem value="copy-link">
<MenuItemIcon>
<LinkIcon />
</MenuItemIcon>
Copy Link
</MenuItem>
<MenuSeparator />
<MenuItem value="messages">
<MenuItemIcon>
<MessageCircleIcon />
</MenuItemIcon>
Send in Messages
</MenuItem>
<MenuItem value="mail">
<MenuItemIcon>
<MailIcon />
</MenuItemIcon>
Send by Mail
</MenuItem>
<MenuSub>
<MenuSubTrigger>
<MenuItemIcon>
<FolderIcon />
</MenuItemIcon>
Add to Album
</MenuSubTrigger>
<MenuSubContent>
<MenuItem value="album-favorites">Favorites</MenuItem>
<MenuItem value="album-trips">Trips</MenuItem>
<MenuItem value="album-family">Family</MenuItem>
</MenuSubContent>
</MenuSub>
<MenuItem value="print">
<MenuItemIcon>
<PrinterIcon />
</MenuItemIcon>
Print
</MenuItem>
<MenuSeparator />
<MenuItem value="delete" destructive>
<MenuItemIcon>
<TrashIcon />
</MenuItemIcon>
Delete Photo
</MenuItem>
</MenuContent>
</Menu>
)
}Open this page on a phone to see the sheet; on a larger screen the same menu opens as a popover.
Platform behavior
- macOS: 24-point rows in a translucent menu material with 5 points of padding. The material is denser than a popover's, so text stays legible over any window. The highlighted row is a solid accent fill with the on-accent color for its text, icon and shortcut. The menu fades and scales in slightly from the trigger; submenus appear in place.
- iOS and iPadOS: the pull-down menu. Rows are 44 points tall with 17-point text, flush to the edges, with hairlines between rows and a thick band between groups. Icons trail the label, a pressed row turns gray instead of accent, and the menu grows out of its button without bouncing. iOS hides shortcuts; iPadOS keeps them for hardware keyboards.
- As a sheet: the menu rises from the bottom edge with a grabber and dims the page. A submenu expands in place under its row, and its chevron turns to point down.
Accessibility
The menu follows the WAI-ARIA menu button pattern. Enter, Space and the arrow keys open it with focus on an item; arrows move, typing jumps to an item by name, Enter runs it, and Escape closes the menu and returns focus to the trigger. Disabled items stay focusable so people can find them. Icons and checkmarks are hidden from screen readers, and shortcuts are announced through aria-keyshortcuts.
API
Every part takes the props of its ui-sdk part. macbetter adds the following.
MenuTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "bezeled" | "filled" | "tinted" | "plain" | "glass" | "bezeled" | The button style. |
tone | "accent" | "neutral" | "destructive" | "accent" | The button's color. |
size | "sm" | "md" | "lg" | "md" | The button's size. |
shape | "auto" | "capsule" | "circle" | "auto" | Rounded rectangle on the Mac, capsule on touch. |
indicator | boolean | true | Shows the pull-down chevron after the label. |
Other parts
| Prop | Type | Default | Description |
|---|---|---|---|
MenuContent | sideOffset | 4 | The popup. Submenus use MenuSubContent, which defaults to 0. |
MenuItem | destructive, shortcut, disabled | — | A command. Destructive items are red. |
MenuCheckboxItem / MenuRadioItem | — | — | Render their own checkmark in the leading column. |
MenuItemIndicator | keepMounted | true | The checkmark, if you compose items yourself. |
MenuSubTrigger | — | — | Renders its own trailing chevron. |
MenuContextTrigger | — | — | An area that opens the menu on right-click or long press. See Context Menu. |
Styling hooks
| Prop | Type | Default | Description |
|---|---|---|---|
data-highlighted | item | — | The row under the pointer or keyboard focus. |
data-checked / data-unchecked | checkbox and radio items | — | Whether the item is on. |
data-destructive | item | — | Set by `destructive`. |
data-has-icons | content | — | Some item has an icon; the rest line up with it. |
data-presentation | content | — | `popover` or `sheet`: how the menu is showing this time. |
--mb-menu-row-padding | length | 9px · 16px | Row inset on the Mac · on touch. |
--mb-menu-gutter | length | 13px · 24px | Width of the check column. |
--mb-menu-highlight | color | accent · gray | The highlighted row's fill. |
--mb-menu-min-width | length | 180px · 250px | The narrowest a menu gets. |
--mb-menu-material | color | thick material, denser | The popup's background. Turns opaque with Reduce Transparency. |
--mb-menu-destructive-text | color | destructive text, lifted in dark | The color of destructive items. |