@lattice-ui/menu Stable direction
import Menu
depends on core , focus , layer , motion , popper Menu is the primitive for a list of actions that opens from a trigger: context menus, dropdown actions, overflow menus, and command lists. It coordinates open state, ordered item movement, positioning, dismissal, and exit motion so your component only has to render the items and their contents.
Reach for Menu when you need a surface of selectable actions that moves selection in order (gamepad up/down and arrow keys), anchors to its trigger through the popper foundation, and dismisses on selection or outside interaction. Menu is modal by default — selection is trapped inside the open menu and restored to the trigger on close.
Import
import { Menu } from "@lattice-ui/menu";Anatomy
Compose the parts you need. Root, Trigger, Portal, and Content form the working menu; Item makes it useful, and Group, Label, and Separator structure longer lists.
<Menu.Root> <Menu.Trigger /> <Menu.Portal> <Menu.Content> <Menu.Label /> <Menu.Group> <Menu.Item /> </Menu.Group> <Menu.Separator /> <Menu.Item /> </Menu.Content> </Menu.Portal></Menu.Root>| Part | Required | Responsibility |
|---|---|---|
Menu.Root | yes | Owns open state, the item registry, and selection movement. |
Menu.Trigger | yes | A button that toggles the menu and acts as the positioning anchor and focus-restore target. |
Menu.Portal | yes | Renders the surface into a ScreenGui outside the local tree. |
Menu.Content | yes | The positioned, focus-trapped, dismissable, motion-driven surface. |
Menu.Item | yes | A selectable action that registers for ordered movement and emits onSelect. |
Menu.Group | no | A vertical layout container that visually groups related items. |
Menu.Label | no | A non-interactive heading for a group or section. |
Menu.Separator | no | A thin divider between items or groups. |
Example
A controlled menu with a grouped action list, a separator, and a destructive item that closes on selection.
import { useState } from "@rbxts/react";import { Menu } from "@lattice-ui/menu";
export function RowActionsMenu() { const [open, setOpen] = useState(false);
return ( <Menu.Root open={open} onOpenChange={setOpen}> <Menu.Trigger asChild> <textbutton Text="Actions" Size={UDim2.fromOffset(120, 36)} /> </Menu.Trigger>
<Menu.Portal> <Menu.Content placement="bottom" sideOffset={6}> <frame AutomaticSize={Enum.AutomaticSize.Y} BackgroundColor3={Color3.fromRGB(28, 32, 42)} Size={UDim2.fromOffset(220, 0)} > <uilistlayout Padding={new UDim(0, 2)} /> <Menu.Label asChild> <textlabel Text="Manage" /> </Menu.Label> <Menu.Group> <Menu.Item onSelect={() => print("rename")}> <textbutton Text="Rename" /> </Menu.Item> <Menu.Item onSelect={() => print("duplicate")}> <textbutton Text="Duplicate" /> </Menu.Item> </Menu.Group> <Menu.Separator /> <Menu.Item onSelect={() => print("delete")}> <textbutton Text="Delete" TextColor3={Color3.fromRGB(244, 120, 120)} /> </Menu.Item> </frame> </Menu.Content> </Menu.Portal> </Menu.Root> );}Omit open/onOpenChange and pass defaultOpen instead to let Menu own its state. Use controlled state only when something outside the menu needs to open or close it.
How it behaves
Open state
Menu.Root is controllable. Pass open and onOpenChange to control it, or defaultOpen to run uncontrolled (defaults to closed). Menu.Trigger toggles the open state on activation (and on the Return/Space keys). Selecting an item closes the menu unless the item’s onSelect calls preventDefault.
Positioning
Menu.Content is positioned by the popper foundation, anchored to the trigger. It measures the trigger and the content, resolves a final placement, and flips to the opposite side when the requested side would collide with the screen edge. Tune it with placement ("top" | "bottom" | "left" | "right", default "bottom"), sideOffset (gap from the trigger, default 0), alignOffset (shift along the cross axis, default 0), and collisionPadding (minimum distance from the screen edge, default 8).
Focus and selection
When the menu opens, the first item is focused automatically and selection is trapped inside the content (Menu is modal by default). Menu.Item registers itself with the root in render order, and the registry drives ordered movement: pressing Up/Down on a focused item moves selection to the previous or next enabled item, wrapping through the list. Disabled items are skipped. When the menu closes, focus is restored to the trigger.
Menu.Item activates on click or on the Return/Space keys, calling onSelect with an event whose preventDefault() keeps the menu open. The default item renders a left-aligned textbutton that highlights on pointer enter and on gamepad selection.
Dismissal
Menu.Content participates in dismissable-layer behavior. Because Menu is modal, interaction behind the surface is blocked and an outside press dismisses it. Use onPointerDownOutside and onInteractOutside to observe or veto those interactions before the menu closes.
Motion and presence
Menu.Content runs a default popper-aware canvas-group reveal/exit recipe that animates from the resolved placement, so it animates in and out without extra setup. Override it with transition, and pass forceMount to keep the content mounted through its exit animation (useful when you drive motion yourself or need the node to persist).
Menu defaults to modal={true}: it traps selection inside the open content and blocks interaction behind it. Set modal={false} on Menu.Root for a lightweight, non-blocking menu that leaves the rest of the UI interactive — closer to Popover’s default behavior.
API reference
Menu.Root
| Prop | Type | Description |
|---|---|---|
| open | boolean | Controlled open state. Pair with onOpenChange. |
| defaultOpen | boolean | Initial open state for uncontrolled usage. Defaults to false. |
| onOpenChange | (open: boolean) => void | Called whenever the open state changes. |
| modal | boolean | When true, traps selection inside the menu and blocks interaction behind it. Defaults to true. |
| children | React.ReactNode | The menu parts. |
Menu.Trigger
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge behavior onto the single child element instead of rendering the default textbutton. |
| disabled | boolean | Prevents the trigger from toggling the menu and removes it from selection. |
| children | React.ReactElement | The element to render. Required when asChild is set. |
Menu.Portal
| Prop | Type | Description |
|---|---|---|
| container | BasePlayerGui | Target PlayerGui to render the surface into. Defaults to the surrounding portal context's container. |
| displayOrderBase | number | Base DisplayOrder for the generated ScreenGui, used to order it against other layers. Defaults to the surrounding portal context's value. |
| children | React.ReactNode | The content part. |
Menu.Content
| Prop | Type | Description |
|---|---|---|
| placement | "top" | "bottom" | "left" | "right" | Requested side to position the content on. Flips on collision. Defaults to "bottom". |
| sideOffset | number | Gap in pixels between the trigger and the content. Defaults to 0. |
| alignOffset | number | Shift in pixels along the trigger's cross axis. Defaults to 0. |
| collisionPadding | number | Minimum distance in pixels to keep from the screen edge. Defaults to 8. |
| asChild | boolean | Render the single child element inside the positioned wrapper instead of the default canvasgroup contents. |
| forceMount | boolean | Keeps the content mounted while exit motion runs. |
| transition | PresenceMotionConfig | Overrides the default popper-aware canvas-group reveal/exit recipe. |
| onPointerDownOutside | (event: LayerInteractEvent) => void | Called when a pointer press occurs outside the content, before dismissal. |
| onInteractOutside | (event: LayerInteractEvent) => void | Called for any other outside interaction, before dismissal. |
| children | React.ReactNode | The menu contents. |
Menu.Item
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge item behavior onto the single child element instead of rendering the default textbutton. |
| disabled | boolean | Prevents selection and skips the item during ordered movement. |
| onSelect | (event: MenuSelectEvent) => void | Called on activation. Call event.preventDefault() to keep the menu open. |
| children | React.ReactElement | The element to render. Required when asChild is set. |
Menu.Group
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge the group onto the single child element instead of rendering the default vertical-layout frame. |
| children | React.ReactElement | The grouped items to render. Required when asChild is set. |
Menu.Label
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge the label onto the single child element instead of rendering the default textlabel. |
| children | React.ReactElement | The label element to render. Required when asChild is set. |
Menu.Separator
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge the separator onto the single child element instead of rendering the default 1px divider frame. |
| children | React.ReactElement | The divider element to render. Required when asChild is set. |