Context Menu
A menu that appears on right-click, providing contextual actions.
ui/context-menu.tsx
"use client";
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
import { CheckIcon, ChevronRightIcon } from "blode-icons-react";
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
const ContextMenu = ({ ...props }: ContextMenuPrimitive.Root.Props) => (
<ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
);
const ContextMenuPortal = ({ ...props }: ContextMenuPrimitive.Portal.Props) => (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
const ContextMenuTrigger = ({ className, ...props }: ContextMenuPrimitive.Trigger.Props) => (
<ContextMenuPrimitive.Trigger
className={cn("select-none", className)}
data-slot="context-menu-trigger"
{...props}
/>
);
const ContextMenuContent = ({
className,
align = "start",
alignOffset = 4,
side = "right",
sideOffset = 0,
children,
...props
}: ContextMenuPrimitive.Popup.Props &
Pick<ContextMenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Positioner
align={align}
alignOffset={alignOffset}
className="isolate z-50 outline-none"
side={side}
sideOffset={sideOffset}
>
<ContextMenuPrimitive.Popup
className={cn(
"cn-menu-target data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 min-w-36 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-popover outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className,
)}
data-slot="context-menu-content"
{...props}
>
<div className="scroll-fade max-h-(--available-height) overflow-x-hidden overflow-y-auto p-1">
{children}
</div>
</ContextMenuPrimitive.Popup>
</ContextMenuPrimitive.Positioner>
</ContextMenuPrimitive.Portal>
);
const ContextMenuGroup = ({ ...props }: ContextMenuPrimitive.Group.Props) => (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
const ContextMenuLabel = ({
className,
inset,
...props
}: ContextMenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}) => (
<ContextMenuPrimitive.GroupLabel
className={cn(
"px-1.5 py-1 font-medium text-muted-foreground text-xs data-inset:pl-7",
className,
)}
data-inset={inset}
data-slot="context-menu-label"
{...props}
/>
);
const ContextMenuItem = ({
className,
inset,
variant = "default",
...props
}: ContextMenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}) => (
<ContextMenuPrimitive.Item
className={cn(
"group/context-menu-item relative flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive",
className,
)}
data-inset={inset}
data-slot="context-menu-item"
data-variant={variant}
{...props}
/>
);
const ContextMenuSub = ({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) => (
<ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
);
const ContextMenuSubTrigger = ({
className,
inset,
children,
...props
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) => (
<ContextMenuPrimitive.SubmenuTrigger
className={cn(
"flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-popup-open:bg-accent data-inset:pl-7 data-popup-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-inset={inset}
data-slot="context-menu-sub-trigger"
{...props}
>
{children}
<ChevronRightIcon className="cn-rtl-flip ml-auto" />
</ContextMenuPrimitive.SubmenuTrigger>
);
const ContextMenuSubContent = ({ ...props }: ComponentProps<typeof ContextMenuContent>) => (
<ContextMenuContent
className="shadow-lg"
data-slot="context-menu-sub-content"
side="right"
{...props}
/>
);
const ContextMenuCheckboxItem = ({
className,
children,
checked,
inset,
...props
}: ContextMenuPrimitive.CheckboxItem.Props & {
inset?: boolean;
}) => (
<ContextMenuPrimitive.CheckboxItem
checked={checked}
className={cn(
"relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-inset={inset}
data-slot="context-menu-checkbox-item"
{...props}
>
<span className="pointer-events-none absolute right-2">
<ContextMenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
const ContextMenuRadioGroup = ({ ...props }: ContextMenuPrimitive.RadioGroup.Props) => (
<ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />
);
const ContextMenuRadioItem = ({
className,
children,
inset,
...props
}: ContextMenuPrimitive.RadioItem.Props & {
inset?: boolean;
}) => (
<ContextMenuPrimitive.RadioItem
className={cn(
"relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-inset={inset}
data-slot="context-menu-radio-item"
{...props}
>
<span className="pointer-events-none absolute right-2">
<ContextMenuPrimitive.RadioItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.RadioItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
const ContextMenuSeparator = ({ className, ...props }: ContextMenuPrimitive.Separator.Props) => (
<ContextMenuPrimitive.Separator
className={cn("-mx-1 my-1 h-px bg-border", className)}
data-slot="context-menu-separator"
{...props}
/>
);
const ContextMenuShortcut = ({ className, ...props }: ComponentProps<"span">) => (
<span
className={cn(
"ml-auto text-muted-foreground text-xs tracking-widest group-focus/context-menu-item:text-accent-foreground",
className,
)}
data-slot="context-menu-shortcut"
{...props}
/>
);
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};
- Interaction
- scroll-revealdropdown
DependenciesBundle weight · light
- ui-primitives
- @base-ui/react