Menu
Contextual actions presented in a floating dropdown, triggered by click or right-click.
Overview
The Menu component displays a list of actions in a floating dropdown panel. It is built as a compound component using @floating-ui/react and supports click triggers, right-click context menus, nested submenus, dividers, labels, and destructive actions with confirmation modals.
Basic Usage
Wrap a trigger element and content panel inside Menu. Each action is a Menu.Item with a label, icon, and onSelect handler.
import { Menu, Button } from "@fsai/shared-ui";
<Menu>
<Menu.Trigger>
<Button role="button" variant="ghost" size="sm">
<Ellipsis className="size-4.5" />
</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Item label="Edit" icon="PencilEdit" onSelect={handleEdit} />
<Menu.Item label="Duplicate" icon="Duplicate" onSelect={handleDuplicate} />
<Menu.Divider />
<Menu.Item
label="Delete"
icon="TrashCan02"
variant="destructive"
onSelect={handleDelete}
/>
</Menu.Content>
</Menu>;Placement
Control where the dropdown appears relative to the trigger with the placement prop. Defaults to bottom-start.
<Menu placement="bottom-end">
<Menu.Trigger>
<Button role="button" variant="secondary" size="sm">
Actions
</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Item label="Export" icon="Download" onSelect={handleExport} />
<Menu.Item label="Archive" icon="Archive" onSelect={handleArchive} />
</Menu.Content>
</Menu>Content Sizes
Menu.Content supports three sizes that control font size and minimum width.
| Size | Usage |
|---|---|
md | Standard menus (default) |
sm | Compact toolbars, inline actions |
xs | Tight spaces, secondary action menus |
<Menu>
<Menu.Trigger>
<Button role="button" variant="ghost" size="xs">
Options
</Button>
</Menu.Trigger>
<Menu.Content size="sm">
<Menu.Item label="Rename" icon="PencilEdit" onSelect={handleRename} />
<Menu.Item
label="Remove"
icon="TrashCan02"
variant="destructive"
onSelect={handleRemove}
/>
</Menu.Content>
</Menu>With Labels and Groups
Use Menu.Label to add section headings and Menu.Divider to visually separate groups of items.
<Menu>
<Menu.Trigger>
<Button role="button" variant="ghost" size="sm">
Add
</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Label>Select lesson</Menu.Label>
<Menu.Item
label="Text Lesson"
icon="Document"
onSelect={() => addLesson("text")}
/>
<Menu.Item
label="Video Lesson"
icon="VideoCamera"
onSelect={() => addLesson("video")}
/>
<Menu.Divider />
<Menu.Item
label="Quiz"
icon="CheckmarkCircle"
onSelect={() => addLesson("quiz")}
/>
</Menu.Content>
</Menu>Nested Submenus
Use Menu.Sub, Menu.SubTrigger, and Menu.SubContent to create hierarchical menus.
<Menu>
<Menu.Trigger>
<Button role="button" variant="ghost" size="sm">
Actions
</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Item label="Download" icon="Download" onSelect={handleDownload} />
<Menu.Divider />
<Menu.Sub>
<Menu.SubTrigger label="Move to Collection" icon="FolderLink2" />
<Menu.SubContent>
<Menu.Item
label="New Collection"
icon="PlusSmall"
onSelect={handleNewCollection}
/>
<Menu.Divider />
<Menu.Item
label="Marketing Assets"
onSelect={() => moveTo("marketing")}
/>
<Menu.Item label="Brand Guidelines" onSelect={() => moveTo("brand")} />
</Menu.SubContent>
</Menu.Sub>
</Menu.Content>
</Menu>Context Menu (Right-Click)
Using Menu.ContextArea
Wrap a region with Menu.ContextArea to open the menu on right-click. The menu positions at the cursor.
<Menu>
<Menu.ContextArea className="relative">
<img src={image.url} alt={image.name} />
</Menu.ContextArea>
<Menu.Content>
<Menu.Item
label="Download to Computer"
icon="Download"
onSelect={handleDownload}
/>
<Menu.Item label="Copy Link" icon="Link" onSelect={handleCopy} />
</Menu.Content>
</Menu>Using contextAreaElement
For imperative use cases (e.g. grids), pass an element ref to position a context menu on right-click.
<Menu contextAreaElement={gridContainerRef}>
<Menu.Content>
<Menu.Item label="Open" icon="Expand" onSelect={handleOpen} />
<Menu.Item
label="Delete"
icon="TrashCan02"
variant="destructive"
onSelect={handleDelete}
/>
</Menu.Content>
</Menu>Using trigger="right-click"
Positions the menu relative to the trigger element (not the cursor) on right-click.
<Menu trigger="right-click" placement="right-start">
<Menu.Trigger>
<LocationPin />
</Menu.Trigger>
<Menu.Content size="sm">
<Menu.Item label="Edit Location" icon="PencilEdit" onSelect={handleEdit} />
<Menu.Item
label="Remove"
icon="TrashCan02"
variant="destructive"
onSelect={handleRemove}
/>
</Menu.Content>
</Menu>Item with Confirmation Modal
Use Menu.ItemWithConfirmation for destructive actions that require a second confirmation step before executing.
<Menu>
<Menu.Trigger>
<Button role="button" variant="ghost" size="sm">
Options
</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Item label="Edit" icon="PencilEdit" onSelect={handleEdit} />
<Menu.ItemWithConfirmation
label="Delete"
icon="TrashCan02"
variant="destructive"
onSelect={handleDelete}
confirmTitle="Delete item?"
confirmDescription="This action cannot be undone."
confirmButtonText="Delete"
/>
</Menu.Content>
</Menu>Use this as the default confirmation path for menu-originated destructive actions. See the Confirmation Modals pattern for when to use this versus raw ConfirmActionModal or ConfirmActionButton.
Disabled Items with Tooltips
When an item is disabled, always attach a tooltip explaining why the action is unavailable. Without it, users are left guessing. The tooltip should appear only while the item is disabled — set tooltip.disabled to the inverse of the disabled condition so the tooltip disappears when the item becomes actionable again.
<Menu.Item
disabled={!isPDF}
label="Prepare eSignature"
icon="PencilWave"
tooltip={{
content: "Only PDFs can be prepared for signature",
placement: "right",
disabled: isPDF,
}}
onSelect={handlePrepareForSignature}
/>Common tooltip patterns
Permission-based: The user lacks the required role or permission.
<Menu.Item
disabled={!hasUnlinkPermission}
label="Unlink from Application"
icon="Underline"
tooltip={{
content: "You don't have permission to unlink this asset",
placement: "right",
disabled: hasUnlinkPermission,
}}
onSelect={handleUnlink}
/>Prerequisite-based: The action requires a prior step to be completed first.
<Menu.Item
disabled={!hasPreparedSignature}
label="Generate eSignature link"
icon="PencilWaveFilled"
tooltip={{
content: "Must be prepared for esignature",
placement: "right",
disabled: hasPreparedSignature,
}}
onSelect={handleGenerateLink}
/>Format-based: The action only applies to certain file types or data shapes.
<Menu.Item
disabled={hasPreparedSignature}
label="Update Version"
icon="HistoryBackTimeline"
tooltip={{
content: "Version control is not available for esignature docs",
placement: "right",
disabled: !hasPreparedSignature,
}}
onSelect={handleUpdateVersion}
/>Custom Item Content
Pass children instead of label/icon for fully custom item rendering.
<Menu.Item onSelect={handleSelect}>
<span className="flex items-center gap-2.5 pr-4">
<StatusIcon className="size-4 shrink-0" />
<span className="font-medium">Active Status</span>
</span>
</Menu.Item>Trigger Conventions
Use consistent trigger buttons so menus are immediately recognisable across the app.
| Context | Trigger pattern | Example |
|---|---|---|
| Row / card overflow actions | ghost button with an ellipsis icon | <Button role="button" variant="ghost" size="sm"><Ellipsis className="size-4.5" /></Button> |
| Toolbar / page-level actions | secondary button with a text label and trailing icon | <Button role="button" variant="secondary" size="xs">Actions <CircleDots className="h-4 ml-1" /></Button> |
| Inline add / create | ghost or secondary button with descriptive label | <Button role="button" variant="ghost" size="sm">Add</Button> |
Match Menu.Content size to the trigger Button size — for example, an xs trigger pairs with size="xs" content.
Item Ordering
Follow a consistent ordering within Menu.Content:
- Primary actions — the most common or expected action first (Edit, View, Open)
- Secondary actions — less frequent actions (Duplicate, Export, Move)
- Divider
- Destructive actions — always last, always separated by a
Menu.Divider, always usingvariant="destructive"
<Menu.Content>
<Menu.Item label="Edit" icon="PencilEdit" onSelect={handleEdit} />
<Menu.Item label="Duplicate" icon="Duplicate" onSelect={handleDuplicate} />
<Menu.Divider />
<Menu.Item
label="Delete"
icon="TrashCan02"
variant="destructive"
onSelect={handleDelete}
/>
</Menu.Content>When to Use Confirmation
Use Menu.ItemWithConfirmation whenever the action is irreversible or has significant side effects — deleting, revoking access, publishing to external audiences, or any bulk operation.
For reversible actions (archive, hide, disable), a plain Menu.Item is sufficient — the user can simply undo.
Guidelines
General
- Do use
Menu.Itemwithlabelandiconfor standard actions — it provides consistent spacing and hover states - Do use
Menu.Dividerto separate logically distinct groups of actions - Do use
placement="bottom-end"when the trigger is right-aligned to prevent overflow - Do provide a
textValueon items with custom children so typeahead navigation works - Don't nest more than one level of submenus — it becomes hard to navigate
- Don't put more than ~10 items in a single menu — group or restructure the UI instead
- Don't mix
Menu.TriggerandMenu.ContextAreain the same menu — choose one activation pattern
Destructive Actions
- Do always place destructive items last, separated by a
Menu.Divider - Do use
variant="destructive"so the item is visually distinct - Do use
Menu.ItemWithConfirmationfor irreversible actions (delete, revoke, publish) - Don't use
variant="destructive"for non-destructive actions - Don't skip confirmation for bulk operations, even if individual deletes don't require it
Disabled Items
- Do always pair
disabledwith atooltipexplaining why the action is unavailable - Do set
tooltip.placementto'right'so the tooltip doesn't overlap the menu panel - Do set
tooltip.disabledto the inverse of the item'sdisabledcondition so the tooltip only shows when the item is disabled - Don't disable items without explanation — remove them from the menu entirely or add a tooltip
Accessibility
- Do use the built-in
labelprop instead of customchildrenwhenever possible — it enables typeahead keyboard navigation automatically - Do set
textValueon items that use customchildrenso typeahead still works - Do keep item labels concise and action-oriented ("Export as JSON", not "Click here to export")
- Don't rely on icons alone — always pair with a text label inside menu items