Skip to content
Components

ContextMenu

A menu that opens at the pointer on right click, on the keyboard context menu keys, or on a touch long press over an area, with the same items, groups, checkbox and radio items, shortcuts and destructive style as DropdownMenu. Every action in it must also be reachable another way.

Preview

Props

  • disabled
  • longPressDelay
  • loop
  • avoidCollisions
  • items
import { IconCopy, IconPencil, IconTrash } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "@/components/ui/context-menu"

<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="flex min-h-28 flex-col justify-center gap-sm rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium text-text-primary">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled leadingIcon={<IconCopy size={16} />}>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>

Usage

When to use

  • Offer the same actions as a visible menu when a user right-clicks, uses a menu key or long-presses an item.

When not to use

  • The only way to reach an action. Keep the same actions available elsewhere.
  • An action menu on touch without a long-press affordance. Use DropdownMenu.

Examples

File row

import { IconPencil, IconTrash } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "@/components/ui/context-menu"

<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>

Checkbox options

import { useState } from "react"
import { IconPencil, IconTrash } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuTrigger } from "@/components/ui/context-menu"

const [menuPinned, setMenuPinned] = useState(false)

<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuLabel>View</ContextMenuLabel>
      <ContextMenuCheckboxItem checked={menuPinned} onCheckedChange={setMenuPinned}>Pin to top</ContextMenuCheckboxItem>
      <ContextMenuSeparator />
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>

Radio options

import { useState } from "react"
import { IconPencil, IconTrash } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuTrigger } from "@/components/ui/context-menu"

const [menuSort, setMenuSort] = useState("name")

<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuLabel>Sort by</ContextMenuLabel>
      <ContextMenuRadioGroup value={menuSort} onValueChange={setMenuSort}>
        <ContextMenuRadioItem value="name">Name</ContextMenuRadioItem>
        <ContextMenuRadioItem value="date">Date</ContextMenuRadioItem>
      </ContextMenuRadioGroup>
      <ContextMenuSeparator />
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>

Disabled area

report.pdf

Right click, long press or Shift+F10

import { IconPencil, IconTrash } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "@/components/ui/context-menu"

<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger disabled className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>

Do and don't

Do

  • Mirror a visible menu or toolbar; the context menu is a shortcut.
  • Use the same item names as the visible menu.
  • Provide an accessible name on ContextMenuContent when the trigger label alone does not explain the actions. ContextMenuTrigger adds tabIndex 0 unless disabled.

Don't

  • Do not make it the only way to an action.
  • Do not override the browser menu on text inputs or links.

Props

NameTypeValuesDefaultDescription
onOpenChange(open: boolean) => void--Called when it opens or closes.
modalboolean-trueBlock outside interaction while open.
disabledboolean-falseOn ContextMenuTrigger: bypasses the custom menu and preserves the browser menu. On item parts: prevents selection.
asChildboolean-falseOn ContextMenuTrigger or ContextMenuItem: use the only child as the rendered element. Interactive links and text fields keep their native context menu.
longPressDelaynumber-500Milliseconds a touch must be held before the menu opens.
loopboolean-falseOn ContextMenuContent: wrap arrow-key navigation from the last enabled item to the first.
avoidCollisionsboolean-trueOn ContextMenuContent: shift or flip the menu to keep it inside the viewport.
collisionPaddingnumber | Partial<Record<"top" | "right" | "bottom" | "left", number>>-8On ContextMenuContent: minimum distance from viewport edges in px.
onSelect(event: Event) => void--On ContextMenuItem: runs when selected by pointer or keyboard. Call event.preventDefault() to keep the menu open.
destructiveboolean-falseOn ContextMenuItem: danger text and icon for a destructive action.
insetboolean-falseOn ContextMenuItem and ContextMenuLabel: align the text with rows that contain icons or selection indicators.
leadingIconnode--On ContextMenuItem: decorative 16px icon. Ignored with asChild.
shortcutstring--On ContextMenuItem: a displayed key hint; the application handles the shortcut.
checkedboolean | "indeterminate"--On ContextMenuCheckboxItem: controlled check state.
onCheckedChange(checked: boolean) => void--On ContextMenuCheckboxItem: receives the next check state.
valuestring--On ContextMenuRadioGroup: the selected value. On ContextMenuRadioItem: the required option identifier.
onValueChange(value: string) => void--On ContextMenuRadioGroup: receives the selected radio item value.

Slots

NameRequiredDescription
childrenyesContextMenuTrigger and ContextMenuContent with item parts.

Anatomy

PartDescription
rootContextMenu: holds open state.
triggerContextMenuTrigger: the area that listens for right click.
contentContextMenuContent: the same floating list as DropdownMenuContent, positioned at the pointer or trigger.
itemContextMenuItem: same as DropdownMenuItem.
iconThe leading icon of an item, as in DropdownMenu.
destructiveA destructive ContextMenuItem, as in DropdownMenu.
destructiveIconThe leading icon of a destructive item, in the danger text color.
indicatorThe check or radio dot of a checkbox or radio item, as in DropdownMenu.
groupLabelContextMenuLabel: a group heading, as in DropdownMenu.
separatorContextMenuSeparator: a line between groups, as in DropdownMenu.
shortcutContextMenuShortcut: the shortcut hint, as in DropdownMenu.

Accessibility

Role: menu

KeysAction
Shift+F10 / ContextMenu keyOpens the menu for the focused trigger area.
ArrowDown / ArrowUpMoves between items.
Enter / SpaceActivates the item.
EscapeCloses the menu and returns focus to the trigger area.
Type a letterMoves to the next item starting with it.
  • Items are the DropdownMenu item parts (shared styles and keyboard), exported under ContextMenu names.
  • The trigger area must be focusable (tabIndex 0 with an accessible name) for keyboard users.
  • The menu is positioned at the pointer, or at the trigger's start when opened from the keyboard.
  • Touch: a long press (about 500ms without moving more than a few px) opens the menu at the touch point. The component implements it itself because iOS Safari does not fire contextmenu on long press. If a native contextmenu event follows the long press, it is ignored so the menu does not open twice.
  • Mouse and keyboard stay for devices with them attached: right click, Shift+F10 and the Menu key.
  • The trigger area sets -webkit-touch-callout: none and user-select: none so the browser callout and text selection do not fight the long press.
  • Submenu parts are not exported. Use groups and separators for sections within this menu.

Install

npx werewolf-ui add context-menu

Run npx werewolf-ui init once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See Installation for requirements and the alternative registry setup.