FSAI Design System
Components

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.

SizeUsage
mdStandard menus (default)
smCompact toolbars, inline actions
xsTight 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.

ContextTrigger patternExample
Row / card overflow actionsghost button with an ellipsis icon<Button role="button" variant="ghost" size="sm"><Ellipsis className="size-4.5" /></Button>
Toolbar / page-level actionssecondary 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 / createghost 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:

  1. Primary actions — the most common or expected action first (Edit, View, Open)
  2. Secondary actions — less frequent actions (Duplicate, Export, Move)
  3. Divider
  4. Destructive actions — always last, always separated by a Menu.Divider, always using variant="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.Item with label and icon for standard actions — it provides consistent spacing and hover states
  • Do use Menu.Divider to separate logically distinct groups of actions
  • Do use placement="bottom-end" when the trigger is right-aligned to prevent overflow
  • Do provide a textValue on 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.Trigger and Menu.ContextArea in 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.ItemWithConfirmation for 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 disabled with a tooltip explaining why the action is unavailable
  • Do set tooltip.placement to 'right' so the tooltip doesn't overlap the menu panel
  • Do set tooltip.disabled to the inverse of the item's disabled condition 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 label prop instead of custom children whenever possible — it enables typeahead keyboard navigation automatically
  • Do set textValue on items that use custom children so 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

On this page