React context menu primitive component with right-click and long-press support built on accessible menu foundations
npx @tessl/cli install tessl/npm-radix-ui--react-context-menu@2.2.0A React context menu primitive component that provides accessible context menus with right-click and long-press support, built on top of Radix UI's menu foundation. Offers complete keyboard navigation, focus management, and customizable styling.
npm install @radix-ui/react-context-menuimport * as ContextMenu from "@radix-ui/react-context-menu";Or import specific components:
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem
} from "@radix-ui/react-context-menu";For CommonJS:
const ContextMenu = require("@radix-ui/react-context-menu");import * as ContextMenu from "@radix-ui/react-context-menu";
function App() {
return (
<ContextMenu.Root>
<ContextMenu.Trigger className="trigger">
Right click me
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content className="context-menu">
<ContextMenu.Item>
Cut
</ContextMenu.Item>
<ContextMenu.Item>
Copy
</ContextMenu.Item>
<ContextMenu.Item>
Paste
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
);
}The context menu system is built around several key architectural components:
Foundation Architecture:
asChild prop supportcreateContextScope for isolated context management and safe composition with other Radix componentsInteraction Model:
Component Inheritance:
side, sideOffset, align) that are automatically managedasChild prop where applicableThe main context menu container that manages state and provides context to child components.
/**
* Root context menu component
*/
interface ContextMenuProps {
children?: React.ReactNode;
onOpenChange?(open: boolean): void;
dir?: Direction;
modal?: boolean; // default: true
}
const ContextMenu: React.FC<ContextMenuProps>;
// Short name alias
const Root: typeof ContextMenu;
type Direction = "ltr" | "rtl";Element that triggers the context menu on right-click or long-press.
/**
* Context menu trigger element
*/
interface ContextMenuTriggerProps extends React.ComponentPropsWithoutRef<typeof Primitive.span> {
disabled?: boolean;
}
// Note: Primitive.span extends standard HTML span element with asChild support
const ContextMenuTrigger: React.ForwardRefExoticComponent<
ContextMenuTriggerProps & React.RefAttributes<HTMLSpanElement>
>;
// Short name alias
const Trigger: typeof ContextMenuTrigger;Portal component for rendering menu content outside the DOM tree.
/**
* Portal for context menu content
*/
interface ContextMenuPortalProps {
children?: React.ReactNode;
container?: HTMLElement | null;
}
const ContextMenuPortal: React.FC<ContextMenuPortalProps>;
// Short name alias
const Portal: typeof ContextMenuPortal;Main container for menu items with positioning and focus management.
/**
* Context menu content container
* Extends MenuPrimitive.Content but omits positioning props that are automatically managed
*/
interface ContextMenuContentProps extends Omit<
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Content>,
"onEntryFocus" | "side" | "sideOffset" | "align"
> {
onEscapeKeyDown?(event: KeyboardEvent): void;
onPointerDownOutside?(event: PointerDownOutsideEvent): void;
onFocusOutside?(event: FocusOutsideEvent): void;
onInteractOutside?(event: InteractOutsideEvent): void;
forceMount?: true;
loop?: boolean;
onCloseAutoFocus?(event: Event): void;
disableOutsidePointerEvents?: boolean;
disableOutsideScroll?: boolean;
trapFocus?: boolean;
// Inherited positioning props (automatically managed)
// side: 'right' (fixed)
// sideOffset: 2 (fixed)
// align: 'start' (fixed)
// Collision detection
avoidCollisions?: boolean;
collisionBoundary?: Element | null | Array<Element | null>;
collisionPadding?: number | Partial<Record<Side, number>>;
// Advanced positioning
arrowPadding?: number;
sticky?: "partial" | "always";
hideWhenDetached?: boolean;
updatePositionStrategy?: "optimized" | "always";
}
const ContextMenuContent: React.ForwardRefExoticComponent<
ContextMenuContentProps & React.RefAttributes<HTMLDivElement>
>;
// Short name alias
const Content: typeof ContextMenuContent;Components for organizing menu items and providing visual structure.
/**
* Groups related menu items together
*/
interface ContextMenuGroupProps extends React.ComponentPropsWithoutRef<"div"> {}
const ContextMenuGroup: React.ForwardRefExoticComponent<
ContextMenuGroupProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Accessible label for menu groups
*/
interface ContextMenuLabelProps extends React.ComponentPropsWithoutRef<"div"> {}
const ContextMenuLabel: React.ForwardRefExoticComponent<
ContextMenuLabelProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Visual separator between menu items
*/
interface ContextMenuSeparatorProps extends React.ComponentPropsWithoutRef<"div"> {}
const ContextMenuSeparator: React.ForwardRefExoticComponent<
ContextMenuSeparatorProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Arrow pointing from menu to trigger
*/
interface ContextMenuArrowProps extends React.ComponentPropsWithoutRef<"svg"> {
width?: number;
height?: number;
}
const ContextMenuArrow: React.ForwardRefExoticComponent<
ContextMenuArrowProps & React.RefAttributes<SVGSVGElement>
>;
// Short name aliases
const Group: typeof ContextMenuGroup;
const Label: typeof ContextMenuLabel;
const Separator: typeof ContextMenuSeparator;
const Arrow: typeof ContextMenuArrow;Interactive components for menu actions and selections.
/**
* Basic interactive menu item
*/
interface ContextMenuItemProps extends React.ComponentPropsWithoutRef<"div"> {
disabled?: boolean;
onSelect?(event: Event): void;
textValue?: string;
}
const ContextMenuItem: React.ForwardRefExoticComponent<
ContextMenuItemProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Menu item with checkbox functionality
*/
interface ContextMenuCheckboxItemProps extends React.ComponentPropsWithoutRef<"div"> {
checked?: boolean | "indeterminate";
onCheckedChange?(checked: boolean): void;
disabled?: boolean;
onSelect?(event: Event): void;
textValue?: string;
}
const ContextMenuCheckboxItem: React.ForwardRefExoticComponent<
ContextMenuCheckboxItemProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Container for radio menu items
*/
interface ContextMenuRadioGroupProps extends React.ComponentPropsWithoutRef<"div"> {
value?: string;
onValueChange?(value: string): void;
}
const ContextMenuRadioGroup: React.ForwardRefExoticComponent<
ContextMenuRadioGroupProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Menu item with radio button functionality
*/
interface ContextMenuRadioItemProps extends React.ComponentPropsWithoutRef<"div"> {
value: string;
disabled?: boolean;
onSelect?(event: Event): void;
textValue?: string;
}
const ContextMenuRadioItem: React.ForwardRefExoticComponent<
ContextMenuRadioItemProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Visual indicator for checked/selected states
*/
interface ContextMenuItemIndicatorProps extends React.ComponentPropsWithoutRef<"span"> {
forceMount?: true;
}
const ContextMenuItemIndicator: React.ForwardRefExoticComponent<
ContextMenuItemIndicatorProps & React.RefAttributes<HTMLSpanElement>
>;
// Short name aliases
const Item: typeof ContextMenuItem;
const CheckboxItem: typeof ContextMenuCheckboxItem;
const RadioGroup: typeof ContextMenuRadioGroup;
const RadioItem: typeof ContextMenuRadioItem;
const ItemIndicator: typeof ContextMenuItemIndicator;Components for creating nested menu structures.
/**
* Container for submenu functionality
*/
interface ContextMenuSubProps {
children?: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?(open: boolean): void;
}
const ContextMenuSub: React.FC<ContextMenuSubProps>;
/**
* Menu item that triggers a submenu
*/
interface ContextMenuSubTriggerProps extends React.ComponentPropsWithoutRef<"div"> {
disabled?: boolean;
textValue?: string;
}
const ContextMenuSubTrigger: React.ForwardRefExoticComponent<
ContextMenuSubTriggerProps & React.RefAttributes<HTMLDivElement>
>;
/**
* Container for submenu items
*/
interface ContextMenuSubContentProps extends React.ComponentPropsWithoutRef<"div"> {
onEscapeKeyDown?(event: KeyboardEvent): void;
onPointerDownOutside?(event: PointerDownOutsideEvent): void;
onFocusOutside?(event: FocusOutsideEvent): void;
onInteractOutside?(event: InteractOutsideEvent): void;
forceMount?: true;
loop?: boolean;
sideOffset?: number;
alignOffset?: number;
avoidCollisions?: boolean;
collisionBoundary?: Element | null | Array<Element | null>;
collisionPadding?: number | Partial<Record<Side, number>>;
arrowPadding?: number;
sticky?: "partial" | "always";
hideWhenDetached?: boolean;
}
const ContextMenuSubContent: React.ForwardRefExoticComponent<
ContextMenuSubContentProps & React.RefAttributes<HTMLDivElement>
>;
// Short name aliases
const Sub: typeof ContextMenuSub;
const SubTrigger: typeof ContextMenuSubTrigger;
const SubContent: typeof ContextMenuSubContent;/**
* Creates a scoped context for composing with other Radix components
* Returns a tuple of context creation functions for advanced composition
*/
function createContextMenuScope(): [
(scope: any) => any,
(scope?: any) => any
];import * as ContextMenu from "@radix-ui/react-context-menu";
function ComplexContextMenu() {
const [checked, setChecked] = React.useState(false);
const [selection, setSelection] = React.useState("option1");
return (
<ContextMenu.Root>
<ContextMenu.Trigger className="trigger">
Right click for full menu
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content className="context-menu">
<ContextMenu.Label>Edit</ContextMenu.Label>
<ContextMenu.Item onSelect={() => console.log("Cut")}>
Cut
</ContextMenu.Item>
<ContextMenu.Item onSelect={() => console.log("Copy")}>
Copy
</ContextMenu.Item>
<ContextMenu.Item onSelect={() => console.log("Paste")}>
Paste
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.CheckboxItem
checked={checked}
onCheckedChange={setChecked}
>
<ContextMenu.ItemIndicator>✓</ContextMenu.ItemIndicator>
Show hidden files
</ContextMenu.CheckboxItem>
<ContextMenu.Separator />
<ContextMenu.Label>View</ContextMenu.Label>
<ContextMenu.RadioGroup value={selection} onValueChange={setSelection}>
<ContextMenu.RadioItem value="option1">
<ContextMenu.ItemIndicator>•</ContextMenu.ItemIndicator>
List view
</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="option2">
<ContextMenu.ItemIndicator>•</ContextMenu.ItemIndicator>
Grid view
</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
<ContextMenu.Separator />
<ContextMenu.Sub>
<ContextMenu.SubTrigger>More options</ContextMenu.SubTrigger>
<ContextMenu.Portal>
<ContextMenu.SubContent>
<ContextMenu.Item>Export</ContextMenu.Item>
<ContextMenu.Item>Import</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Portal>
</ContextMenu.Sub>
<ContextMenu.Arrow />
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
);
}import * as ContextMenu from "@radix-ui/react-context-menu";
function ControlledContextMenu() {
const [open, setOpen] = React.useState(false);
return (
<ContextMenu.Root open={open} onOpenChange={setOpen}>
<ContextMenu.Trigger>
Controlled trigger (open: {String(open)})
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item onSelect={() => setOpen(false)}>
Close menu
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
);
}// Event types for outside interaction detection
type PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;
type FocusOutsideEvent = CustomEvent<{ originalEvent: FocusEvent }>;
type InteractOutsideEvent = PointerDownOutsideEvent | FocusOutsideEvent;
// Positioning and layout types
type Side = "top" | "right" | "bottom" | "left";
type Align = "start" | "center" | "end";
type Direction = "ltr" | "rtl";
// Primitive component type (from @radix-ui/react-primitive)
type Primitive = {
span: React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<"span"> & { asChild?: boolean }>;
div: React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<"div"> & { asChild?: boolean }>;
svg: React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<"svg"> & { asChild?: boolean }>;
};
// Menu primitive types (from @radix-ui/react-menu)
type MenuPrimitive = {
Content: React.ForwardRefExoticComponent<MenuContentProps>;
Item: React.ForwardRefExoticComponent<MenuItemProps>;
CheckboxItem: React.ForwardRefExoticComponent<MenuCheckboxItemProps>;
RadioGroup: React.ForwardRefExoticComponent<MenuRadioGroupProps>;
RadioItem: React.ForwardRefExoticComponent<MenuRadioItemProps>;
ItemIndicator: React.ForwardRefExoticComponent<MenuItemIndicatorProps>;
Group: React.ForwardRefExoticComponent<MenuGroupProps>;
Label: React.ForwardRefExoticComponent<MenuLabelProps>;
Separator: React.ForwardRefExoticComponent<MenuSeparatorProps>;
Arrow: React.ForwardRefExoticComponent<MenuArrowProps>;
SubTrigger: React.ForwardRefExoticComponent<MenuSubTriggerProps>;
SubContent: React.ForwardRefExoticComponent<MenuSubContentProps>;
Portal: React.ForwardRefExoticComponent<MenuPortalProps>;
};The components accept standard HTML attributes and can be styled with CSS. Radix UI provides CSS custom properties for advanced positioning and styling:
.context-menu {
/* Custom properties available */
transform-origin: var(--radix-context-menu-content-transform-origin);
width: var(--radix-context-menu-content-available-width);
height: var(--radix-context-menu-content-available-height);
}