Skip to content

Foundations

Action Menu

Dropdown and right-click menus — items with icons and shortcuts, submenus, checkable items and a destructive group.

2 variantsMIT

Preview

Installation

Copies the component into components/ui and installs its dependencies and theme tokens.

pnpm dlx shadcn@latest add https://tuffui.com/r/action-menu.json

Usage

Pass your own data and wire the callbacks to your agent runtime.

import { Menu, ContextMenu, type MenuGroup } from "@/components/ui/action-menu";

const groups: MenuGroup[] = [
  {
    items: [
      { id: "copy", label: "Copy", icon: Copy, shortcut: "⌘C", onSelect: copy },
      { id: "regen", type: "submenu", label: "Regenerate with", items: [
        { id: "model", type: "radio", value: model, onValueChange: setModel,
          options: [{ value: "opus", label: "opus-5.5" }, { value: "sonnet", label: "sonnet-5.5" }] },
      ] },
    ],
  },
  { label: "View", items: [
    { id: "reasoning", type: "checkbox", label: "Show reasoning", checked, onCheckedChange: setChecked },
  ] },
  { items: [{ id: "delete", label: "Delete", icon: Trash, destructive: true, onSelect: remove }] },
];

<Menu groups={groups} trigger={<IconButton icon={DotsThree} label="Actions" />} />

<ContextMenu groups={groups}>
  <div tabIndex={0}>Right-click me</div>
</ContextMenu>

Props

PropTypeDefaultDescription
groups*MenuGroup[]—Item groups, separated by spacing and an optional small label.
trigger*ReactElement—Menu only. A button-like element that opens it.
children*ReactElement—ContextMenu only. The area that opens it on right-click, long-press or the context-menu key.
side / align"top" | "right" | "bottom" | "left" / "start" | "center" | "end""bottom" / "start"Menu only. Placement against the trigger; flips when there's no room.
open / onOpenChangeboolean / (open: boolean) => void—Optional control of the open state.
type MenuGroup = { id?: string; label?: string; items: MenuItem[] };

type MenuItem =
  | { type?: "item"; id: string; label: ReactNode; icon?: Icon; shortcut?: string;
      description?: ReactNode; destructive?: boolean; disabled?: boolean; onSelect?: () => void }
  | { type: "checkbox"; id: string; label: ReactNode; icon?: Icon;
      checked: boolean; onCheckedChange: (checked: boolean) => void }
  | { type: "radio"; id: string; value: string; onValueChange: (value: string) => void;
      options: { value: string; label: ReactNode; icon?: Icon; disabled?: boolean }[] }
  | { type: "submenu"; id: string; label: ReactNode; icon?: Icon; items: MenuItem[] };