Action menu
A button-triggered menu of commands, independent settings, exclusive choices and nested actions.
On this page
Choose a command to see its result. This example changes no saved report.
Code
import { ActionMenu, type ActionMenuEntry } from "@bosunkit/components/action-menu";
import { Button } from "@bosunkit/components/button";
import { TaskDialog } from "@bosunkit/components/task-dialog";
import { Copy, Download, Pencil, Trash2 } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const title: string = "Commands, nested exports and explicit deletion";
const entries: readonly ActionMenuEntry[] = [
{kind:"group",id:"report",label:"Technical report",entries:[
{id:"edit",label:"Edit report",icon:<Pencil />,description:"Change the title and report content."},
{id:"duplicate",label:"Duplicate report",icon:<Copy />},
{kind:"submenu",id:"download",label:"Download",icon:<Download />,entries:[{id:"pdf",label:"PDF document"},{id:"markdown",label:"Markdown source"}]},
{id:"publish",label:"Publish report",description:"Review is required before publication.",disabled:true},
]},
{kind:"separator",id:"divider"},
{id:"delete",label:"Delete report",icon:<Trash2 />,destructive:true},
];
export default function Example(): ReactElement {
const [result,setResult]: [string,Dispatch<SetStateAction<string>>] = useState("Choose a command to see its result. This example changes no saved report.");
const [confirm,setConfirm]: [boolean,Dispatch<SetStateAction<boolean>>] = useState(false);
function choose(id:string): void {
if(id === "delete") { setConfirm(true); return; }
const results: Readonly<Record<string,string>> = {edit:"Example editor requested.",duplicate:"Example duplicate requested.",pdf:"Example PDF download requested.",markdown:"Example Markdown download requested."};
setResult(results[id] ?? "Command requested.");
}
return <div><ActionMenu label="Report actions" entries={entries} onSelect={choose} />
<p role="status">{result}</p>
<TaskDialog title="Delete the example report?" description="This demonstrates confirmation. No saved report is affected." open={confirm} onOpenChange={(open): void => { if (!open) ((): void => { setConfirm(false); })(); }}
actions={<><Button variant="ghost" onClick={(): void => { setConfirm(false); }}>Keep report</Button><Button variant="danger" onClick={(): void => {setConfirm(false);setResult("Example deletion confirmed.");}}>Delete example</Button></>}>
<p>Keep confirmation in the task that owns the report.</p>
</TaskDialog>
</div>;
}
Commands and groups
An ordinary entry calls onSelect(id) and closes by default. Use description to explain an action and
disabled to retain an unavailable command. Named group entries and separator entries organize related
commands. Destructive entries have explicit wording and a distinct text treatment; the component does not
delete data or supply a confirmation dialog itself.
submenu opens a nested menu with the same theme, size class and language. Keep important commands shallow.
Keyboard users can open with Enter, Space or Arrow Down, navigate with arrows or typeahead, choose with Enter,
and dismiss one level at a time with Escape. Closing returns focus to the trigger.
Persistent choices
Notes shown; captions hidden. Sorted by publication date.
Code
import { ActionMenu } from "@bosunkit/components/action-menu";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const title: string = "Persistent checkboxes and mutually exclusive options";
export default function Example(): ReactElement {
const [notes,setNotes]: [boolean,Dispatch<SetStateAction<boolean>>] = useState(true);
const [figures,setFigures]: [boolean,Dispatch<SetStateAction<boolean>>] = useState(false);
const [sort,setSort]: [string,Dispatch<SetStateAction<string>>] = useState("date");
return <div><ActionMenu label="View options" entries={[
{kind:"group",id:"include",label:"Include",entries:[{kind:"checkbox",id:"notes",label:"Source notes",checked:notes,onCheckedChange:setNotes},{kind:"checkbox",id:"figures",label:"Figure captions",checked:figures,onCheckedChange:setFigures}]},
{kind:"separator",id:"divider"},
{kind:"radio-group",id:"sort",label:"Sort by",value:sort,onValueChange:setSort,options:[{id:"date",label:"Publication date"},{id:"title",label:"Title"},{id:"category",label:"Category"}]},
]} /><p role="status">Notes {notes ? "shown" : "hidden"}; captions {figures ? "shown" : "hidden"}. Sorted by {sort === "date" ? "publication date" : sort}.</p></div>;
}
Checkbox and radio entries stay open by default so readers can make related changes. Set closeOnSelect when
a choice should dismiss the menu. Each choice accepts controlled state and a corresponding default for
uncontrolled use. These are menu choices rather than native form fields; use the standalone controls in forms.
Custom triggers and shortcuts
Shortcut hints describe commands registered by the application.
Code
import { ActionMenu } from "@bosunkit/components/action-menu";
import { IconButton } from "@bosunkit/components/icon-button";
import { Ellipsis } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const title: string = "Named icon trigger and shortcut hints";
export default function Example(): ReactElement {
const [result,setResult]: [string,Dispatch<SetStateAction<string>>] = useState("Shortcut hints describe commands registered by the application.");
return <div><ActionMenu label="More document actions" triggerTooltip align="end"
trigger={<IconButton label="More document actions" icon={<Ellipsis />} />}
entries={[{id:"copy",label:"Copy link",shortcut:"Ctrl Shift C"},{id:"download",label:"Download source",shortcut:"Ctrl Shift D"}]}
onSelect={(id:string): void => { setResult(id === "copy" ? "Example copy requested." : "Example download requested."); }} />
<p role="status">{result}</p></div>;
}
trigger accepts a button that forwards its ref and received DOM props. The default is a named kit Button with a
chevron. triggerTooltip exposes the name on hover and focus for compact triggers. Shortcut hints use
Kbd; the menu does not register keyboard shortcuts. Only show a shortcut when the caller
implements it.
open, defaultOpen and onOpenChange control visibility. side and align choose preferred placement, while
collision handling keeps the menu available near a viewport edge. The popup scrolls within its available height
and uses the dropdown layer. Reduced motion removes its scale transition.
Reuse in navigation
The navigation account control uses ActionMenu for its account commands. A caller can also place the same menu beside a table row, in a page header, or in a split button. It does not read account data, authorize a command or navigate on its own.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | – | Complete translated trigger and popup name. |
| entriesrequired | readonly ActionMenuEntry[] | – | Ordered actions and choices. |
| onSelect | ((id: string) => void) | undefined | – | Reports an ordinary command; choices use their own change callbacks. |
| trigger | ReactElement | undefined | – | Custom button, forwarding its ref and trigger props; a labeled Button is the default. |
| triggerTooltip | boolean | undefined | false | Show the menu name as a tooltip, useful for an icon-only custom trigger. |
| disabled | boolean | undefined | – | Disable the trigger, for example while an action is pending. |
| open | boolean | undefined | – | Controlled visibility. |
| defaultOpen | boolean | undefined | – | Initial visibility when uncontrolled. |
| onOpenChange | BaseMenu.Root.Props["onOpenChange"] | – | Reports opening and dismissal. |
| side | "top" | "bottom" | "left" | "right" | undefined | "bottom" | Preferred popup side; collision handling can flip it. |
| align | "start" | "center" | "end" | undefined | "start" | Alignment along the trigger. |
| motion | boolean | undefined | true | Enable token-based entrance/exit; reduced motion removes scaling. |
| className | string | undefined | – | Additional popup classes. |
| testId | string | undefined | – | Root trigger uses --trigger and popup uses --menu. |
| ref | Ref<HTMLButtonElement> | undefined | – | Trigger element. |
Related
- Split button: a primary action alongside alternatives.
- Context menu: shortcuts opened by right click or long press.
- Command palette: searchable destinations and actions.
- Task dialog: confirmation and pending task boundaries.