Shadcn Context Menu for React and Tailwind
A menu shown on right-click.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/context-menu.jsonpnpm dlx shadcn@latest add https://kit.dev/r/context-menu.jsonnpx shadcn@latest add https://kit.dev/r/context-menu.jsonyarn shadcn@latest add https://kit.dev/r/context-menu.json<Step>This component depends on Menu. Install it first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react tailwind-variants lucide-reactpnpm add @ark-ui/react tailwind-variants lucide-reactnpm install @ark-ui/react tailwind-variants lucide-reactyarn add @ark-ui/react tailwind-variants lucide-reactCopy and paste the following code into your project.
"use client";
import {
Menu as ArkMenu,
useMenu as useArkMenu,
useMenuContext as useArkMenuContext,
useMenuItemContext as useArkMenuItemContext,
} from "@ark-ui/react/menu";
import type React from "react";
import { cn } from "@/lib/utils";
import {
Menu,
MenuArrow,
MenuCheckboxItem,
MenuContent,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuQuickItem,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuShortcut,
MenuSub,
MenuSubContent,
MenuSubTrigger,
} from "@/components/ui/menu";
export const useContextMenu = useArkMenu;
export const useContextMenuContext = useArkMenuContext;
export const useContextMenuItemContext = useArkMenuItemContext;
export const ContextMenuContext: typeof ArkMenu.Context = ArkMenu.Context;
export const ContextMenuItemContext: typeof ArkMenu.ItemContext =
ArkMenu.ItemContext;
export const ContextMenu = (props: React.ComponentProps<typeof Menu>) => {
const { positioning = { placement: "bottom-start" }, ...rest } = props;
return <Menu data-slot="context-menu" positioning={positioning} {...rest} />;
};
export const ContextMenuRootProvider = (
props: React.ComponentProps<typeof ArkMenu.RootProvider>
) => {
const { lazyMount = true, unmountOnExit = true, ...rest } = props;
return (
<ArkMenu.RootProvider
data-slot="context-menu"
lazyMount={lazyMount}
unmountOnExit={unmountOnExit}
{...rest}
/>
);
};
export const ContextMenuTrigger = (
props: React.ComponentProps<typeof ArkMenu.ContextTrigger>
) => {
const { children, className, ...rest } = props;
const child =
Array.isArray(children) && children.length === 1 ? children[0] : children;
return (
<ArkMenu.ContextTrigger
className={cn("cursor-default", className)}
data-slot="context-menu-trigger"
{...rest}
>
{child}
</ArkMenu.ContextTrigger>
);
};
export const ContextMenuContent = (
props: React.ComponentProps<typeof MenuContent>
) => <MenuContent data-slot="context-menu-content" {...props} />;
export const ContextMenuGroup = (
props: React.ComponentProps<typeof MenuGroup>
) => <MenuGroup data-slot="context-menu-group" {...props} />;
export const ContextMenuGroupLabel = (
props: React.ComponentProps<typeof MenuGroupLabel>
) => <MenuGroupLabel data-slot="context-menu-group-label" {...props} />;
export const ContextMenuSeparator = (
props: React.ComponentProps<typeof MenuSeparator>
) => <MenuSeparator data-slot="context-menu-separator" {...props} />;
export const ContextMenuItem = (
props: React.ComponentProps<typeof MenuItem>
) => <MenuItem data-slot="context-menu-item" {...props} />;
export const ContextMenuQuickItem = (
props: React.ComponentProps<typeof MenuQuickItem>
) => <MenuQuickItem data-slot="context-menu-quick-item" {...props} />;
export const ContextMenuCheckboxItem = (
props: React.ComponentProps<typeof MenuCheckboxItem>
) => <MenuCheckboxItem data-slot="context-menu-checkbox-item" {...props} />;
export const ContextMenuRadioGroup = (
props: React.ComponentProps<typeof MenuRadioGroup>
) => <MenuRadioGroup data-slot="context-menu-radio-group" {...props} />;
export const ContextMenuRadioItem = (
props: React.ComponentProps<typeof MenuRadioItem>
) => <MenuRadioItem data-slot="context-menu-radio-item" {...props} />;
export const ContextMenuSub = (props: React.ComponentProps<typeof MenuSub>) => (
<MenuSub data-slot="context-menu-sub" {...props} />
);
export const ContextMenuSubContent = (
props: React.ComponentProps<typeof MenuSubContent>
) => <MenuSubContent data-slot="context-menu-sub-content" {...props} />;
export const ContextMenuSubTrigger = (
props: React.ComponentProps<typeof MenuSubTrigger>
) => <MenuSubTrigger data-slot="context-menu-sub-trigger" {...props} />;
export const ContextMenuShortcut = (
props: React.ComponentProps<typeof MenuShortcut>
) => <MenuShortcut data-slot="context-menu-shortcut" {...props} />;
export const ContextMenuArrow = (
props: React.ComponentProps<typeof MenuArrow>
) => <MenuArrow data-slot="context-menu-arrow" {...props} />;Update the import paths to match your project setup.
Anatomy
ContextMenu
├── ContextMenuTrigger
└── ContextMenuContent
├── ContextMenuGroup
│ └── ContextMenuGroupLabel
├── ContextMenuItem
├── ContextMenuQuickItem
├── ContextMenuCheckboxItem
├── ContextMenuRadioGroup
│ └── ContextMenuRadioItem
├── ContextMenuSub
│ ├── ContextMenuSubTrigger
│ └── ContextMenuSubContent
├── ContextMenuSeparator
├── ContextMenuShortcut
└── ContextMenuArrowContextMenu is Menu with ContextMenuTrigger (right-click / long-press) instead of MenuTrigger (click). Content, items, groups, submenus, checkboxes, and radios are the same Menu parts. useContextMenu is the machine hook for ContextMenuRootProvider and returns { api, service }. useContextMenuContext / ContextMenuContext is in-tree (the api).
ContextMenuContent portals the list and includes the positioner. The trigger sets anchorPoint from the pointer.
Usage
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";<ContextMenu>
<ContextMenuTrigger>Right click here</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem value="action-1">Action 1</ContextMenuItem>
<ContextMenuItem value="action-2">Action 2</ContextMenuItem>
<ContextMenuItem value="action-3">Action 3</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>shadcn.io Menu defaults lazyMount and unmountOnExit to true (Ark: false). Context Menu defaults positioning.placement to "bottom-start" (Menu: "bottom-end"). Give every item a stable value.
Controlled
Control open state with open and onOpenChange. { open: boolean }.
Root Provider
Use useContextMenu with ContextMenuRootProvider when you need the API outside the tree. Pass machine options (positioning, closeOnSelect, onSelect, …) to useContextMenu(), not to the provider. The hook returns { api, service } — call menu.api.setOpen(true) and pass the whole return value to the provider.
Examples
Nested menu
Use ContextMenuSub, ContextMenuSubTrigger, and ContextMenuSubContent for nested submenus.
Icons
Combine icons with labels for quick scanning. Decorative icons should be aria-hidden="true".
Shortcuts
Use ContextMenuShortcut to show keyboard hints next to items.
Checkboxes
Use ContextMenuCheckboxItem for toggles. Control with checked and onCheckedChange.
Radio group
Use ContextMenuRadioGroup and ContextMenuRadioItem for a single choice. onValueChange receives { value: string }.
With link
Use asChild on ContextMenuItem with an <a> or Next.js Link.
With group label
Use ContextMenuGroup with heading or ContextMenuGroupLabel.
With separator
Use ContextMenuSeparator between related groups.
Quick items
Use ContextMenuQuickItem for icon-above-text actions in a horizontal row.
Scrollable
Add max-h-* and overflow (already overflow-y-auto on content) to cap height.
Open a dialog
Open a Dialog from onSelect on an item.
Destructive
Use variant="destructive" on ContextMenuItem for irreversible actions.
Context
Read open and highlighted state with ContextMenuContext or useContextMenuContext.
Guides
Context Menu vs Menu
| Context Menu | Menu | |
|---|---|---|
| Trigger | Right-click / long-press (ContextMenuTrigger) | Click (MenuTrigger) |
| Anchor | Pointer (anchorPoint) | Trigger element |
| Placement default | bottom-start | bottom-end |
| Content | Same Menu parts | Same Menu parts |
Use Menu for a button-triggered dropdown. Use Command when the list is searchable.
Pointer position
ContextMenuTrigger sets anchorPoint from the pointer. positioning.placement is relative to that point. Override with positioning on ContextMenu (or on useContextMenu()).
<ContextMenu positioning={{ placement: "right-start", gutter: 8 }}>Custom trigger surface
ContextMenuTrigger renders a button by default. Merge onto a card, image, or other host with asChild and a single child.
<ContextMenuTrigger asChild>
<div className="rounded-xl border p-6">Right click the card</div>
</ContextMenuTrigger>The host should be focusable if keyboard users need the same actions (Shift+F10 / context-menu key). Offer a visible Menu on small viewports where long-press is not obvious.
Item values
Every ContextMenuItem, checkbox, radio, and submenu item needs a unique value. Typeahead and highlight tracking use it.
API Reference
shadcn.io Context Menu wraps Menu (Ark Menu machine). Defaults below are shadcn.io Context Menu values. lazyMount / unmountOnExit are true (Ark: false). positioning.placement is "bottom-start" (Menu: "bottom-end"). data-scope stays menu; shadcn.io sets data-slot="context-menu" on the root.
asChild merges props onto a single child element.
ContextMenu
Root. Same as Menu besides the placement default. Renders no extra chrome; content is created by ContextMenuContent.
| Prop | Type | Default | Description |
|---|---|---|---|
anchorPoint | Point | null | - | Positioning point. Set by the context trigger from the pointer. |
aria-label | string | - | Accessibility label for the menu. |
asChild | boolean | false | Render the child element instead of the root. |
className | string | - | Class names on the root. |
closeOnSelect | boolean | true | Close the menu when an item is selected. |
composite | boolean | true | Treat as composed with other composite widgets. |
defaultHighlightedValue | string | null | - | Uncontrolled initial highlighted value. |
defaultOpen | boolean | - | Uncontrolled initial open state. |
defaultTriggerValue | string | null | - | Uncontrolled initial trigger value. |
hideMode | "display-none" | "activity" | "display-none" | How to hide mounted-but-closed content. activity needs React 19+. |
highlightedValue | string | null | - | Controlled highlighted value. |
id | string | - | Unique id for the machine. |
ids | Partial<{ trigger: string | ((value?: string) => string); contextTrigger: string | ((value?: string) => string); content: string; groupLabel: (id: string) => string; group: (id: string) => string; positioner: string; arrow: string }> | - | Element ids for composition. |
immediate | boolean | - | Apply presence changes immediately instead of the next frame. |
lazyMount | boolean | true | Mount content on first open. |
loopFocus | boolean | false | Loop keyboard focus through items. |
navigate | (details: NavigateDetails) => void | - | Called when a link item is chosen. { value, node, href }. |
onExitComplete | () => void | - | Called when the close animation finishes. |
onFocusOutside | (event: FocusOutsideEvent) => void | - | Called when focus moves outside. |
onHighlightChange | (details: HighlightChangeDetails) => void | - | Called when the highlighted item changes. { highlightedValue }. |
onInteractOutside | (event: InteractOutsideEvent) => void | - | Called on outside interaction. |
onOpenChange | (details: OpenChangeDetails) => void | - | Called when open state changes. { open }. |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | - | Called on pointer down outside. |
onSelect | (details: SelectionDetails) => void | - | Called when an item is selected. { value }. |
onTriggerValueChange | (details: TriggerValueChangeDetails) => void | - | Called when the active trigger changes. { value, triggerElement }. |
open | boolean | - | Controlled open state. |
positioning | PositioningOptions | { placement: "bottom-start" } | Floating position relative to anchorPoint. |
present | boolean | - | Controlled presence. |
skipAnimationOnMount | boolean | false | Skip the initial presence animation. |
triggerValue | string | null | - | Controlled trigger value. |
typeahead | boolean | true | Jump to items by typing printable characters. |
unmountOnExit | boolean | true | Unmount content after the close animation. |
| Attribute | Description |
|---|---|
data-slot | context-menu |
data-scope | menu |
data-part | root |
ContextMenuTrigger
Opens the menu on right-click or long-press. Renders a button.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the trigger. |
value | string | - | Id for this trigger when several share one menu. |
| Attribute | Description |
|---|---|
data-slot | context-menu-trigger |
data-scope | menu |
data-part | context-trigger |
data-state | "open" or "closed" |
data-value | The trigger value |
ContextMenuContent
Portaled list surface. Includes the positioner. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the content. |
| Attribute | Description |
|---|---|
data-slot | context-menu-content |
data-scope | menu |
data-part | content |
data-state | "open" or "closed" |
data-placement | Placement of the content |
data-nested | Present when nested in another menu |
data-has-nested | Present when this menu has nested menus |
| CSS variable | Description |
|---|---|
--available-width | Available width in the viewport (on the positioner) |
--available-height | Available height in the viewport (on the positioner) |
--reference-width | Width of the trigger (on the positioner) |
--reference-height | Height of the trigger (on the positioner) |
--transform-origin | Transform origin for open/close animation |
--layer-index | Index in the dismissable layer stack |
--nested-layer-count | Number of nested menus |
ContextMenuItem
A single action. Renders a div. value is required.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Stable id. Used for highlight and typeahead. |
valueText | string | - | Text used for typeahead when it differs from the children. |
variant | "default" | "destructive" | "default" | Visual variant. |
disabled | boolean | - | Disable the item. |
closeOnSelect | boolean | - | Override root closeOnSelect for this item. |
onSelect | () => void | - | Called when this item is chosen. |
asChild | boolean | false | Merge onto a single child (for example an <a>). |
className | string | - | Class names on the item. |
| Attribute | Description |
|---|---|
data-slot | context-menu-item |
data-scope | menu |
data-part | item |
data-highlighted | Present when highlighted |
data-disabled | Present when disabled |
data-value | The item value |
data-variant | "default" or "destructive" |
ContextMenuCheckboxItem
Toggle item. Indicator is built in.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Stable id. |
checked | boolean | - | Controlled checked state. |
onCheckedChange | (checked: boolean) => void | - | Called when checked state changes. |
disabled | boolean | - | Disable the item. |
valueText | string | - | Text used for typeahead. |
closeOnSelect | boolean | - | Override root closeOnSelect. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the item. |
| Attribute | Description |
|---|---|
data-slot | context-menu-checkbox-item |
data-scope | menu |
data-part | item |
data-state | "checked" or "unchecked" |
data-highlighted | Present when highlighted |
data-disabled | Present when disabled |
ContextMenuRadioGroup
Mutually exclusive options. Pass heading to render ContextMenuGroupLabel.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Controlled selected value. |
onValueChange | (details: { value: string }) => void | - | Called when the selection changes. |
heading | string | - | Group label. |
id | string | - | Group id. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the group. |
| Attribute | Description |
|---|---|
data-slot | context-menu-radio-group |
data-scope | menu |
data-part | item-group |
ContextMenuRadioItem
A single option in a radio group. Indicator is built in.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Option value. |
disabled | boolean | - | Disable the item. |
valueText | string | - | Text used for typeahead. |
closeOnSelect | boolean | - | Override root closeOnSelect. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the item. |
| Attribute | Description |
|---|---|
data-slot | context-menu-radio-item |
data-scope | menu |
data-part | item |
data-state | "checked" or "unchecked" |
data-highlighted | Present when highlighted |
data-disabled | Present when disabled |
ContextMenuGroup
Groups related items. Pass heading to render ContextMenuGroupLabel.
| Prop | Type | Default | Description |
|---|---|---|---|
heading | string | - | Group label. |
id | string | - | Group id. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the group. |
| Attribute | Description |
|---|---|
data-slot | context-menu-group |
data-scope | menu |
data-part | item-group |
ContextMenuGroupLabel
Label for a group. Usually created by heading on ContextMenuGroup. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the label. |
| Attribute | Description |
|---|---|
data-slot | context-menu-group-label |
data-scope | menu |
data-part | item-group-label |
ContextMenuSeparator
Visual divider. Renders a hr.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the separator. |
| Attribute | Description |
|---|---|
data-slot | context-menu-separator |
data-scope | menu |
data-part | separator |
ContextMenuQuickItem
Item with icon-above-text layout. Same props as ContextMenuItem. Use in a horizontal flex row.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Stable id. |
variant | "default" | "destructive" | "default" | Visual variant. |
disabled | boolean | - | Disable the item. |
closeOnSelect | boolean | - | Override root closeOnSelect. |
onSelect | () => void | - | Called when this item is chosen. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the item. |
| Attribute | Description |
|---|---|
data-slot | context-menu-quick-item |
data-scope | menu |
data-part | item |
ContextMenuSub / ContextMenuSubTrigger / ContextMenuSubContent
Nested menu. Wrap ContextMenuSubTrigger and ContextMenuSubContent in ContextMenuSub. ContextMenuSub is another Menu root (data-slot="context-menu-sub"). ContextMenuSubContent is portaled.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the part. |
ContextMenuSubTrigger renders a trigger item with a chevron. ContextMenuSub accepts the same root props as ContextMenu.
| Attribute | Description |
|---|---|
data-slot | context-menu-sub, context-menu-sub-trigger, context-menu-sub-content |
data-scope | menu |
data-part | root, trigger-item, content |
data-state | "open" or "closed" (trigger and content) |
data-highlighted | Present when the subtrigger is highlighted |
ContextMenuShortcut
Keyboard hint aligned to the end of an item. Renders a span.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the shortcut. |
| Attribute | Description |
|---|---|
data-slot | context-menu-shortcut |
ContextMenuArrow
Optional arrow toward the anchor. Less common for pointer menus.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Class names on the arrow. |
asChild | boolean | false | Merge onto a single child. |
| CSS variable | Default |
|---|---|
--arrow-background | var(--popover) |
--arrow-size | calc(1.5 * var(--spacing)) |
| Attribute | Description |
|---|---|
data-slot | context-menu-arrow |
data-scope | menu |
data-part | arrow |
ContextMenuRootProvider
Root alternative that takes { api, service } from useContextMenu(). Presence (lazyMount, unmountOnExit) can still be set on the provider.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseMenuReturn | required | Return value of useContextMenu(). |
hideMode | "display-none" | "activity" | "display-none" | How to hide mounted-but-closed content. |
immediate | boolean | - | Apply presence changes immediately. |
lazyMount | boolean | true | Mount content on first open. |
onExitComplete | () => void | - | Called when the close animation finishes. |
present | boolean | - | Controlled presence. |
skipAnimationOnMount | boolean | false | Skip the initial presence animation. |
unmountOnExit | boolean | true | Unmount after the close animation. |
Pass positioning, closeOnSelect, and other machine options to useContextMenu(), not to ContextMenuRootProvider. To match ContextMenu, pass positioning: { placement: "bottom-start" }.
| Attribute | Description |
|---|---|
data-slot | context-menu |
data-scope | menu |
data-part | root |
useContextMenu
Creates the menu API for ContextMenuRootProvider. Returns { api, service } — not the api alone.
const menu = useContextMenu({
positioning: { placement: "bottom-start" },
});
menu.api.setOpen(true);ContextMenuContext / useContextMenuContext
Render-prop or hook access to menu state (the api). Use inside ContextMenu or ContextMenuRootProvider.
| Property | Type | Description |
|---|---|---|
open | boolean | Whether the menu is open. |
setOpen | (open: boolean) => void | Open or close the menu. |
triggerValue | string | null | Active trigger value. |
setTriggerValue | (value: string | null) => void | Set the trigger value. |
highlightedValue | string | null | Highlighted item value. |
setHighlightedValue | (value: string) => void | Highlight an item by value. |
reposition | (options?: Partial<PositioningOptions>) => void | Update menu position. |
getItemState | (props: { value: string; disabled?: boolean }) => ItemState | State for one item (id, disabled, highlighted). |
getOptionItemState | (props: OptionItemProps) => OptionItemState | State for a checkbox or radio item (includes checked). |
ContextMenuContext children: (context) => ReactNode.
ContextMenuItemContext / useContextMenuItemContext
Render-prop or hook access to one item. Use inside an item.
| Property | Type | Description |
|---|---|---|
id | string | Item id. |
disabled | boolean | Whether the item is disabled. |
highlighted | boolean | Whether the item is highlighted. |
checked | boolean | Whether a checkbox/radio item is checked (optional). |
ContextMenuItemContext children: (context) => ReactNode.
Accessibility
Complies with the Menu WAI-ARIA design pattern. The trigger should be reachable from the keyboard when the same actions are required without a pointer. Label icon-only items. Keep destructive actions on variant="destructive" and confirm them in a Dialog when needed.
Keyboard support
| Key | Description |
|---|---|
Shift + F10 | Opens the menu when the trigger is focused. |
Context Menu | Opens the menu when the trigger is focused (when the keyboard has this key). |
ArrowDown | Moves to the next item. Opens the menu when used on the trigger. |
ArrowUp | Moves to the previous item. |
Home | Moves to the first item. |
End | Moves to the last item. |
Enter / Space | Selects the highlighted item. |
ArrowRight | Opens a submenu (LTR). |
ArrowLeft | Closes a submenu (LTR). |
Escape | Closes the menu. |
| Printable characters | Typeahead: jumps to a matching item when typeahead is on. |