Skip to content
design
Components

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.

Open a report's commands, explore export formats, or request an explicitly confirmed deletion.
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>;
}
01A discoverable triggerUse a visible button for secondary actions. Give icon-only triggers a complete accessible name and a tooltip.
02Commands and choices differA command requests an action. Checkboxes retain independent settings; radio groups retain one choice.
03The task owns the outcomeMenu selection is a request. The application owns confirmation, pending work, failure and the completed result.

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.

Do
Name the menu for the object whose actions it offers.
Don't
Avoid an ambiguous menu name.

Props

PropTypeDefaultDescription
labelrequiredstring–Complete translated trigger and popup name.
entriesrequiredreadonly ActionMenuEntry[]–Ordered actions and choices.
onSelect((id: string) => void) | undefined–Reports an ordinary command; choices use their own change callbacks.
triggerReactElement | undefined–Custom button, forwarding its ref and trigger props; a labeled Button is the default.
triggerTooltipboolean | undefinedfalseShow the menu name as a tooltip, useful for an icon-only custom trigger.
disabledboolean | undefined–Disable the trigger, for example while an action is pending.
openboolean | undefined–Controlled visibility.
defaultOpenboolean | undefined–Initial visibility when uncontrolled.
onOpenChangeBaseMenu.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.
motionboolean | undefinedtrueEnable token-based entrance/exit; reduced motion removes scaling.
classNamestring | undefined–Additional popup classes.
testIdstring | undefined–Root trigger uses --trigger and popup uses --menu.
refRef<HTMLButtonElement> | undefined–Trigger element.