Menu
Context menu modeled on modern mobile behavior, especially iOS.
Installation
npx axiom add menupnpm dlx axiom add menuyarn dlx axiom add menubun x axiom add menuAlso copies: portal, overlay, use-overlay-back-handler, which closes the menu on a back action. See navigation.
Installs: react-native-reanimated, react-native-safe-area-context, react-native-worklets.
Requires the arrow-left, check and chevron-right icons in your icon registry. Positioning and the animation live in use-menu.ts. Colors come from menu.default.{default,pressed,destructive,disabled} in the component tokens.
Usage
import { Menu } from "@/components/ui/menu";
<Menu.Root>
<Menu.Trigger>
<PhotoThumbnail />
</Menu.Trigger>
<Menu.Content>
<Menu.Item icon="share" onPress={share}>
Share
</Menu.Item>
<Menu.Separator />
<Menu.Item icon="trash" destructive onPress={remove}>
Delete
</Menu.Item>
</Menu.Content>
</Menu.Root>;Props
Menu.Root
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled visibility. |
defaultOpen | boolean | false | Initial visibility when the menu manages its own state. |
onOpenChange | (open: boolean) => void | — | Called when the menu opens or closes. |
children | ReactNode | — | Trigger and Content. |
Menu.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
action | 'longPress' | 'press' | 'longPress' | longPress for context menus on content, press for a “more” button. |
preview | boolean | ReactNode | true with longPress | Lifts a copy of the pressed element above the backdrop. Pass a node to show it in the element’s place instead. |
asChild | boolean | false | Uses the child as the trigger. |
accessibilityLabel | string | — | Label of the trigger, when its content has no text. |
children | ReactNode | — | The element that opens the menu. |
style | StyleProp<ViewStyle> | — | Extra styles for the trigger. |
Menu.Content
| Prop | Type | Default | Description |
|---|---|---|---|
placement | 'auto' | 'top' | 'bottom' | 'auto' | Side of the trigger. auto picks the side with more room and keeps the menu inside the safe area. |
align | 'start' | 'center' | 'end' | 'start' | Horizontal alignment against the trigger. |
width | number | 250 | Menu width. |
overlay | 'dim' | 'none' | 'dim' | Backdrop behind the menu. Pressing it closes the menu, even when none. A blur needs expo-blur: add it in your copy if you want one. |
children | ReactNode | — | Items, groups and separators. |
style | StyleProp<ViewStyle> | — | Extra styles for the menu surface. |
Menu.Item
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | — | The label. |
icon | IconName | — | Icon on the right, iOS style. |
subtitle | string | — | Second line under the label. |
onPress | () => void | — | Called after the menu closes, so a navigation or a sheet doesn’t fight the close animation. |
destructive | boolean | false | Label and icon in feedback.error. |
disabled | boolean | false | Dims the item and ignores presses. |
checked | boolean | — | Shows a checkmark, for a menu that picks a sort order or a view. |
keepOpen | boolean | false | Runs onPress right away without closing the menu, for a toggle inside the menu. |
Menu.Group, Menu.Separator, Menu.Sub
| Part | Props | Description |
|---|---|---|
Group | label | Groups items under an optional small label. |
Separator | — | A thick gap between groups, like iOS. |
Sub | label, icon, children | An item that opens a nested menu in place. |
Menu.Sub replaces the items in place and adds a back row at the top. Menu.Item also takes keepOpen, to run onPress without closing the menu. The menu has accessibilityRole="menu", its items menuitem, and a back action closes it.
Not implemented yet: dragging from the trigger to an item, and haptics (the haptics library depends on the project, see the roadmap).
Use cases
“More” button in an app bar
action="press" on an IconButton. No preview, the menu hangs from the button.
<Menu.Root>
<Menu.Trigger action="press" asChild>
<IconButton icon="more" accessibilityLabel="More options" />
</Menu.Trigger>
<Menu.Content align="end">
<Menu.Group label="Sort by">
<Menu.Item checked={sort === "date"} onPress={() => setSort("date")}>
Date
</Menu.Item>
<Menu.Item checked={sort === "name"} onPress={() => setSort("name")}>
Name
</Menu.Item>
</Menu.Group>
<Menu.Separator />
<Menu.Item icon="edit" onPress={rename}>
Rename
</Menu.Item>
<Menu.Item icon="trash" destructive onPress={confirmDelete}>
Delete
</Menu.Item>
</Menu.Content>
</Menu.Root>Long press on a message
The message is lifted as a preview above the backdrop; quick reactions sit above it.
<Menu.Root>
<Menu.Trigger preview>
<MessageBubble message={message} />
</Menu.Trigger>
<Menu.Content>
<Reactions onReact={react} />
<Menu.Item icon="arrow-up" onPress={reply}>
Reply
</Menu.Item>
<Menu.Item icon="copy" onPress={copy}>
Copy
</Menu.Item>
<Menu.Item icon="trash" destructive onPress={remove}>
Delete
</Menu.Item>
</Menu.Content>
</Menu.Root>Row actions in a list
A long press on a row, as an alternative to swipe actions that is easier to discover.
<Menu.Root>
<Menu.Trigger>
<Item onPress={() => open(file)}>…</Item>
</Menu.Trigger>
<Menu.Content>
<Menu.Sub label="Open with…">{apps.map(renderApp)}</Menu.Sub>
<Menu.Item icon="archive" onPress={move}>
Move
</Menu.Item>
<Menu.Item icon="edit" disabled={file.locked} onPress={rename}>
Rename
</Menu.Item>
</Menu.Content>
</Menu.Root>Related
- BottomSheet
- Long press, to open it from a row