Menu
An action menu on the APG menu-button pattern — roving focus, typeahead, and full keyboard support.
"use client";
import { Button, Kbd, Menu, toast, Toaster } from "astralis-ui";
import { Copy, Pencil, Settings, Trash2 } from "lucide-react";
export function MenuDemo() {
const did = (what: string) => toast.info(what);
return (
<>
<Toaster />
<Menu>
<Menu.Trigger>
<Button variant="outline" colorScheme="gray">Actions</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Label>Mission</Menu.Label>
<Menu.Item icon={<Pencil className="astralis:size-full" />} shortcut={<Kbd size="sm">E</Kbd>} onSelect={() => did("Edit")}>
Edit
</Menu.Item>
<Menu.Item icon={<Copy className="astralis:size-full" />} shortcut={<Kbd size="sm">⌘C</Kbd>} onSelect={() => did("Duplicate")}>
Duplicate
</Menu.Item>
<Menu.Item icon={<Settings className="astralis:size-full" />} disabled>
Configure
</Menu.Item>
<Menu.Separator />
<Menu.Item danger icon={<Trash2 className="astralis:size-full" />} onSelect={() => did("Deleted")}>
Delete
</Menu.Item>
</Menu.Content>
</Menu>
</>
);
}Import#
import { Menu } from "astralis-ui";Placement#
"use client";
import { Button, HStack, Menu } from "astralis-ui";
function Placement({ side, align, label }: { side?: "top" | "bottom" | "left" | "right"; align?: "start" | "center" | "end"; label: string }) {
return (
<Menu side={side} align={align}>
<Menu.Trigger>
<Button size="sm" variant="subtle" colorScheme="gray">{label}</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Item>First</Menu.Item>
<Menu.Item>Second</Menu.Item>
<Menu.Item>Third</Menu.Item>
</Menu.Content>
</Menu>
);
}
export function MenuPlacement() {
return (
<HStack gap="3" wrap="wrap">
<Placement label="bottom / start" />
<Placement side="bottom" align="end" label="bottom / end" />
<Placement side="top" align="start" label="top / start" />
<Placement side="right" align="start" label="right / start" />
</HStack>
);
}Props#
<Menu> (root)#
| Prop | Type | Default | Description |
|---|---|---|---|
open / defaultOpen / onOpenChange | — | — | Controlled/uncontrolled open state. |
side | "top" | "bottom" | "left" | "right" | "bottom" | Preferred side (flips on collision). |
align | "start" | "center" | "end" | "start" | Alignment along the trigger. |
sideOffset | number | 6 | Gap to the trigger, px. |
closeOnSelect | boolean | true | Close after an item activates. |
<Menu.Item>#
| Prop | Type | Default | Description |
|---|---|---|---|
onSelect | () => void | — | Runs on click / Enter / Space. |
disabled | boolean | false | Skipped by roving focus. |
danger | boolean | false | Destructive styling. |
icon / shortcut | ReactNode | — | Leading glyph / trailing hint (pairs well with Kbd). |
closeOnSelect | boolean | inherits | Per-item override. |
Parts#
Menu.Trigger (wraps your button; wires aria-haspopup/aria-expanded),
Menu.Content, Menu.Label, Menu.Separator, Menu.Group.
Keyboard#
| Key | Action |
|---|---|
Enter / Space / ↓ on trigger | Open (focus moves to the first item) |
↑ ↓ | Move focus (wraps) |
Home / End | First / last item |
| type characters | Jump to the item starting with what you typed |
Enter / Space | Activate the focused item |
Esc | Close, return focus to the trigger |
Tab | Close |
Accessibility#
role="menu"/role="menuitem"witharia-haspopup="menu"andaria-expandedon the trigger; disabled items arearia-disabledand skipped by focus movement.- Hover focuses items, so pointer and keyboard share one highlight state.
- Escape/outside-click dismissal runs through the shared overlay stack — a Menu inside a Modal peels first.