Context Menu
Displays an interactive context menu popup upon right-click or tap-and-hold interaction, supporting target element binding, nested submenus with smooth hover delays, keyboard shortcuts, and selectable toggles.
import { ContextMenu, ContextMenuTrigger, ContextMenuContent } from "@/components/ui/contextMenu/contextMenu";npx @bloomui-react/cli add contextMenu"use client";
import { Icon } from "@iconify/react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import * as React from "react";
import { cn } from "@/lib/utils";
const ContextMenu = ({
modal = false,
...props
}: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Root>) => (
<ContextMenuPrimitive.Root modal={modal} {...props} />
);
interface ContextMenuTriggerProps
extends React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Trigger> {
target?: HTMLElement | React.RefObject<HTMLElement | null> | string;
}
const ContextMenuTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Trigger>,
ContextMenuTriggerProps
>(({ target, children, ...props }, ref) => {
const triggerRef = React.useRef<HTMLSpanElement>(null);
React.useEffect(() => {
if (!target) return;
let element: HTMLElement | null = null;
if (typeof target === "string") {
element = document.querySelector(target);
} else if ("current" in target) {
element = target.current;
} else if (target instanceof HTMLElement) {
element = target;
}
if (!element) return;
const handleContextMenu = (e: MouseEvent) => {
e.preventDefault();
if (triggerRef.current) {
const syntheticEvent = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: e.clientX,
clientY: e.clientY,
screenX: e.screenX,
screenY: e.screenY,
});
triggerRef.current.dispatchEvent(syntheticEvent);
}
};
element.addEventListener("contextmenu", handleContextMenu);
return () => {
element?.removeEventListener("contextmenu", handleContextMenu);
};
}, [target]);
return (
<ContextMenuPrimitive.Trigger
ref={ref}
asChild={Boolean(target)}
{...props}
>
{target ? <span ref={triggerRef} className="hidden" /> : children}
</ContextMenuPrimitive.Trigger>
);
});
ContextMenuTrigger.displayName = ContextMenuPrimitive.Trigger.displayName;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-pointer select-none items-center rounded-lg px-2.5 py-1.5 text-xs font-medium outline-none text-zinc-900 dark:text-zinc-100 focus:bg-zinc-100 dark:focus:bg-zinc-800 data-[state=open]:bg-zinc-100 dark:data-[state=open]:bg-zinc-800 transition-colors duration-150",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<Icon
icon="hugeicons:arrow-right-01"
className="ml-auto size-4 text-zinc-400"
/>
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-1.5 text-zinc-900 dark:text-zinc-100 shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[10rem] overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-1.5 text-zinc-900 dark:text-zinc-100 shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
color?: "default" | "danger";
}
>(({ className, inset, color = "default", ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-lg px-2.5 py-1.5 text-xs font-medium outline-none transition-colors",
color === "danger"
? "text-rose-600 dark:text-rose-400 focus:bg-rose-50 dark:focus:bg-rose-950/40 focus:text-rose-600 dark:focus:text-rose-400"
: "text-zinc-900 dark:text-zinc-100 focus:bg-zinc-100 dark:focus:bg-zinc-800",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-lg py-1.5 pl-8 pr-2.5 text-xs font-medium outline-none transition-colors text-zinc-900 dark:text-zinc-100 focus:bg-zinc-100 dark:focus:bg-zinc-800 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2.5 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Icon
icon="hugeicons:tick-02"
className="size-3.5 text-sky-600 dark:text-sky-400"
/>
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-lg py-1.5 pl-8 pr-2.5 text-xs font-medium outline-none transition-colors text-zinc-900 dark:text-zinc-100 focus:bg-zinc-100 dark:focus:bg-zinc-800 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2.5 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Icon
icon="hugeicons:record"
className="size-2 fill-current text-sky-600 dark:text-sky-400"
/>
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2.5 py-1.5 text-xs font-semibold text-zinc-500 dark:text-zinc-400",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1.5 my-1 h-px bg-zinc-200 dark:bg-zinc-800", className)}
{...props}
/>
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-zinc-400 dark:text-zinc-500 font-mono",
className,
)}
{...props}
/>
);
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuPortal,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
};
Default
Standard context menu triggered on any HTML element via child nesting.
Custom Target Element Binding (ContextMenuTrigger target={...})
Bind the right-click context menu trigger to any standalone DOM element or ref using the target prop on ContextMenuTrigger.
Target Element Attached via Ref
Right click anywhere on this custom card box
Submenu Nesting with Smooth Hover Delay
Hierarchical submenu nesting with smooth delay transitions. Hover over items with sub-options to expand them.
Shortcut Key Hints Display Column
Render aligned keyboard shortcut hints on the right side of menu items using ContextMenuShortcut.
With Checkboxes and Radio Groups
Selectable toggle options and single-choice radio items within a context menu.
Sub-components — ContextMenu
Available primitives for building right-click context menus.
| Component | Description |
|---|---|
| ContextMenu | Main root wrapper component with modal=false scroll locking fix. |
| ContextMenuTrigger | Target element area receiving right clicks. |
| ContextMenuContent | Floating popover content panel holding context menu items. |
| ContextMenuItem | Selectable context action option item with optional color="danger". |
| ContextMenuCheckboxItem | Checkbox item supporting boolean toggle states. |
| ContextMenuRadioGroup / RadioItem | Single-selection radio group within context menu. |
| ContextMenuSub / SubTrigger / SubContent | Nested sub-level popup menu primitives. |
| ContextMenuShortcut | Keyboard shortcut hint badge rendered on the right. |