@lattice-ui/menu Part of the stable direction toward v1.0.
What It Is For
Use Menu for contextual action lists where opening the surface should immediately hand focus to the first available item.
It is appropriate for action menus and command surfaces, not value-selection widgets like Select.
import { PortalProvider } from "@lattice-ui/layer";
import { Menu } from "@lattice-ui/menu";
import React from "@rbxts/react";
type Props = {
playerGui: PlayerGui;
};
function MenuDemo() {
const [lastAction, setLastAction] = React.useState("none");
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(360, 220)}>
<Menu.Root>
<Menu.Trigger asChild>
<textbutton
AutoButtonColor={false}
BackgroundColor3={Color3.fromRGB(53, 104, 196)}
BorderSizePixel={0}
Size={UDim2.fromOffset(180, 40)}
Text="Open menu"
TextColor3={Color3.fromRGB(240, 244, 250)}
TextSize={14}
>
<uicorner CornerRadius={new UDim(0, 8)} />
</textbutton>
</Menu.Trigger>
<Menu.Portal>
<Menu.Content asChild offset={new Vector2(0, 8)} placement="bottom">
<frame
BackgroundColor3={Color3.fromRGB(22, 28, 39)}
BorderSizePixel={0}
Size={UDim2.fromOffset(220, 144)}
>
<uicorner CornerRadius={new UDim(0, 10)} />
<uipadding
PaddingLeft={new UDim(0, 8)}
PaddingRight={new UDim(0, 8)}
PaddingTop={new UDim(0, 8)}
PaddingBottom={new UDim(0, 8)}
/>
<uilistlayout
FillDirection={Enum.FillDirection.Vertical}
Padding={new UDim(0, 4)}
/>
<Menu.Label asChild>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(204, 18)}
Text="Actions"
TextColor3={Color3.fromRGB(172, 181, 196)}
TextSize={12}
TextXAlignment={Enum.TextXAlignment.Left}
/>
</Menu.Label>
<Menu.Item asChild onSelect={() => setLastAction("Duplicate")}>
<textbutton
AutoButtonColor={false}
BackgroundColor3={Color3.fromRGB(34, 41, 54)}
BorderSizePixel={0}
Size={UDim2.fromOffset(204, 30)}
Text="Duplicate"
TextColor3={Color3.fromRGB(236, 240, 248)}
TextSize={14}
TextXAlignment={Enum.TextXAlignment.Left}
>
<uicorner CornerRadius={new UDim(0, 6)} />
<uipadding PaddingLeft={new UDim(0, 10)} />
</textbutton>
</Menu.Item>
<Menu.Item asChild onSelect={() => setLastAction("Archive")}>
<textbutton
AutoButtonColor={false}
BackgroundColor3={Color3.fromRGB(34, 41, 54)}
BorderSizePixel={0}
Size={UDim2.fromOffset(204, 30)}
Text="Archive"
TextColor3={Color3.fromRGB(236, 240, 248)}
TextSize={14}
TextXAlignment={Enum.TextXAlignment.Left}
>
<uicorner CornerRadius={new UDim(0, 6)} />
<uipadding PaddingLeft={new UDim(0, 10)} />
</textbutton>
</Menu.Item>
<Menu.Separator asChild>
<frame
BackgroundColor3={Color3.fromRGB(60, 76, 104)}
BorderSizePixel={0}
Size={UDim2.fromOffset(204, 1)}
/>
</Menu.Separator>
<Menu.Item asChild disabled={true}>
<textbutton
Active={false}
AutoButtonColor={false}
BackgroundColor3={Color3.fromRGB(34, 41, 54)}
BorderSizePixel={0}
Selectable={false}
Size={UDim2.fromOffset(204, 30)}
Text="Delete (disabled)"
TextColor3={Color3.fromRGB(140, 148, 162)}
TextSize={14}
TextXAlignment={Enum.TextXAlignment.Left}
>
<uicorner CornerRadius={new UDim(0, 6)} />
<uipadding PaddingLeft={new UDim(0, 10)} />
</textbutton>
</Menu.Item>
</frame>
</Menu.Content>
</Menu.Portal>
</Menu.Root>
<textlabel
BackgroundTransparency={1}
Position={UDim2.fromOffset(0, 62)}
Size={UDim2.fromOffset(220, 20)}
Text={`Last action: ${lastAction}`}
TextColor3={Color3.fromRGB(172, 181, 196)}
TextSize={13}
TextXAlignment={Enum.TextXAlignment.Left}
/>
</frame>
);
}
export function MenuExample(props: Props) {
return (
<PortalProvider container={props.playerGui}>
<MenuDemo />
</PortalProvider>
);
} Live demo is experimental and may contain bugs.
Install
Global CLI command: lattice add menu
Monorepo local script
Use your package manager wrapper when running the local lattice command.
pnpm lattice add menuPublic Exports
-
Menu -
Menu.Root -
Menu.Trigger -
Menu.Portal -
Menu.Content -
Menu.Item -
Menu.Group -
Menu.Label -
Menu.Separator -
MenuContent -
MenuGroup -
MenuItem -
MenuLabel -
MenuPortal -
MenuRoot -
MenuSeparator -
MenuTrigger
State Model
Menu.Rootsupports controlled and uncontrolledopenstate and amodalmode flag.- Registered menu items are ordered and selection moves through that ordered set when the menu is open.
- When the menu opens, the first enabled item receives focus; when it closes, focus can return to the trigger.
Key API
Menu.Root
Use open, defaultOpen, onOpenChange, and modal to define menu ownership and dismissal behavior.
Menu.Content
Use placement, offset, and outside-interaction hooks to position the surface and customize dismissal.
Menu.Item
Use disabled and onSelect for per-action behavior; calling preventDefault keeps the menu open when needed.
Composition Patterns
Overflow or kebab menus
Pair a compact trigger with grouped Menu.Item actions and separators for secondary actions that should not live in the main layout.
Contextual action blocks
Use labels and groups to separate destructive or advanced actions without giving up caller-owned visuals.
Cautions / Limits
- This package models a flat action menu; there are no submenu primitives in the current public surface.
- Menus open into item-focus mode immediately, so they are not a replacement for searchable or value-driven pickers.