Context Menu
A wrapper for Base UI Context Menu — same options API as Menu, triggered by right-click on any element.
Default
Groups, submenus, separators
Right-click here
Checkbox
Indicator right (default)
Right-click here
Indicator left
Right-click here
With group labels
Right-click here
Radio
Controlled
Right-click here
Indicator left
Right-click here
With group labels
Right-click here
Installation
npx shadcn@latest add @glrk-ui/context-menuIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
type allowedPrimitiveT = string | number | boolean
type optionT = {
label: React.ReactNode
value: allowedPrimitiveT
className?: string
disabled?: boolean
}
type groupT = {
group: string
options: (allowedPrimitiveT | optionT)[]
className?: string
}
type optionsT = (allowedPrimitiveT | optionT | groupT)[]
type indicatorAtT = 'right' | 'left'
type menuOptionT =
| allowedPrimitiveT
| optionT
| (optionT & {
variant?: 'default' | 'destructive'
shortcut?: string
disabled?: boolean
})
type menuGroupT = {
group: string
options: menuOptionT[]
className?: string
groupLabelCls?: string
}
type subMenuT = {
submenu: string
options: (menuOptionT | menuGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuOptionsT = (menuOptionT | menuGroupT | subMenuT)[]
type menuInputOptionT =
| allowedPrimitiveT
| optionT
| (optionT & {
disabled?: boolean
})
type menuInputGroupT = {
group: string
options: menuInputOptionT[]
className?: string
groupLabelCls?: string
}
type inputSubMenuT = {
submenu: string
options: (menuInputOptionT | menuInputGroupT)[]
triggerCls?: string
contentCls?: string
}
type menuInputOptionsT = (menuInputOptionT | menuInputGroupT | inputSubMenuT)[]
import { isValidElement, type ReactNode } from 'react'
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function isAllowedPrimitive(value: unknown): value is allowedPrimitiveT {
return ['string', 'number', 'boolean'].includes(typeof value)
}
export function parseAllowedPrimitive(value: allowedPrimitiveT): allowedPrimitiveT {
if (typeof value !== 'string') return value
const trimmed = value.trim()
if (trimmed === 'true') return true
if (trimmed === 'false') return false
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed)
return trimmed
}
export function optionTypeChecker<T>(key: keyof T) {
return (option: any): option is T => !!option && typeof option === 'object' && key in option
}
export const isSeparator = (item: any) => item === '---'
export const isOption = optionTypeChecker<optionT>('value')
export const isGroup = optionTypeChecker<groupT>('group')
export const getValue = (item: allowedPrimitiveT | optionT) =>
typeof item === 'object' ? item.value : item
export const getLabel = (item: allowedPrimitiveT | optionT) =>
typeof item === 'object' ? item.label : `${item}`
export function getKey(item: allowedPrimitiveT | optionT, i: number): string {
const val = getValue(item)
if (typeof val === 'boolean') return `key-${val}`
if (val === '---') return `---${i}`
return `${val}`
}
export function extractText(node: ReactNode): string {
if (node == null || typeof node === 'boolean') return ''
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (isValidElement(node)) return extractText((node.props as { children?: ReactNode }).children)
if (Array.isArray(node)) return node.map(extractText).join('')
return ''
}
import { optionTypeChecker } from './utils'
export const isSubMenu = optionTypeChecker<subMenuT>('submenu')
export const isGroupMenu = optionTypeChecker<menuGroupT>('group')
export const isInputSubMenu = optionTypeChecker<inputSubMenuT>('submenu')
export const isInputGroupMenu = optionTypeChecker<menuInputGroupT>('group')
'use client'
import * as React from 'react'
import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu'
import { ChevronRightIcon, CheckIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function ContextMenu(props: ContextMenuPrimitive.Root.Props) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuPortal(props: ContextMenuPrimitive.Portal.Props) {
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
}
function ContextMenuTrigger({ className, ...props }: ContextMenuPrimitive.Trigger.Props) {
return (
<ContextMenuPrimitive.Trigger
data-slot="context-menu-trigger"
className={cn('select-none', className)}
{...props}
/>
)
}
function ContextMenuContent({
className,
align = 'start',
alignOffset = 4,
side = 'right',
sideOffset = 0,
...props
}: ContextMenuPrimitive.Popup.Props &
Pick<ContextMenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Positioner
className="outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<ContextMenuPrimitive.Popup
data-slot="context-menu-content"
className={cn(
'max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
className,
)}
{...props}
/>
</ContextMenuPrimitive.Positioner>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuGroup(props: ContextMenuPrimitive.Group.Props) {
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
}
function ContextMenuLabel({
className,
inset,
...props
}: ContextMenuPrimitive.GroupLabel.Props & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.GroupLabel
data-slot="context-menu-label"
data-inset={inset}
className={cn(
'px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7',
className,
)}
{...props}
/>
)
}
function ContextMenuItem({
className,
inset,
variant = 'default',
...props
}: ContextMenuPrimitive.Item.Props & {
inset?: boolean
variant?: 'default' | 'destructive'
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive",
className,
)}
{...props}
/>
)
}
function ContextMenuSub(props: ContextMenuPrimitive.SubmenuRoot.Props) {
return <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubmenuTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubmenuTrigger>
)
}
function ContextMenuSubContent(props: React.ComponentProps<typeof ContextMenuContent>) {
return (
<ContextMenuContent
data-slot="context-menu-sub-content"
className="shadow-lg"
side="right"
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
inset,
indicatorAt = 'right',
...props
}: ContextMenuPrimitive.CheckboxItem.Props & {
inset?: boolean
indicatorAt?: indicatorAtT
}) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
indicatorAt === 'right' ? 'pr-8 pl-2' : 'pr-2 pl-8',
)}
checked={checked}
{...props}
>
<span
className={cn(
'pointer-events-none absolute',
indicatorAt === 'right' ? 'right-2' : 'left-2',
)}
>
<ContextMenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioGroup(props: ContextMenuPrimitive.RadioGroup.Props) {
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />
}
function ContextMenuRadioItem({
className,
children,
inset,
indicatorAt = 'right',
...props
}: ContextMenuPrimitive.RadioItem.Props & {
inset?: boolean
indicatorAt?: indicatorAtT
}) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
indicatorAt === 'right' ? 'pr-8 pl-2' : 'pr-2 pl-8',
)}
{...props}
>
<span
className={cn(
'pointer-events-none absolute',
indicatorAt === 'right' ? 'right-2' : 'left-2',
)}
>
<ContextMenuPrimitive.RadioItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.RadioItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuSeparator({ className, ...props }: ContextMenuPrimitive.Separator.Props) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn('-mx-1 my-1 h-px bg-border', className)}
{...props}
/>
)
}
function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground',
className,
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
'use client'
import { useState } from 'react'
import {
cn,
getKey,
getLabel,
getValue,
isSeparator,
parseAllowedPrimitive,
} from '@/lib/utils'
import {
isSubMenu,
isGroupMenu,
isInputSubMenu,
isInputGroupMenu
} from '@/lib/menu'
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuGroup,
ContextMenuLabel,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubTrigger,
ContextMenuSubContent,
} from '@/components/ui/context-menu'
type commonCheckboxProps = {
checked?: allowedPrimitiveT[]
indicatorAt?: indicatorAtT
onCheckedChange?: (value: allowedPrimitiveT, checked: boolean) => void
}
type commonRadioProps = {
value?: allowedPrimitiveT
indicatorAt?: indicatorAtT
onValueChange?: (value: allowedPrimitiveT) => void
}
type commonSubMenuT = {
itemCls?: string
groupCls?: string
groupLabelCls?: string
}
type commonPropsT = {
children: React.ReactNode
itemCls?: string
groupCls?: string
groupLabelCls?: string
contentProps?: React.ComponentProps<typeof ContextMenuContent>
onSelect?: (value: allowedPrimitiveT) => void
} & React.ComponentProps<typeof ContextMenu>
// -------
type itemProps = {
option: menuOptionT
className?: string
onSelect?: () => void
}
function Item({ option, className, onSelect }: itemProps) {
const value = getValue(option)
if (isSeparator(value)) return <ContextMenuSeparator className={cn(className)} />
const label = getLabel(option)
const opt: any = typeof option === 'object' ? option : {}
const shortcut = opt?.shortcut
return (
<ContextMenuItem {...opt} onSelect={onSelect} className={cn(className, opt?.className)}>
{label}
{shortcut && <ContextMenuShortcut>{shortcut}</ContextMenuShortcut>}
</ContextMenuItem>
)
}
type checkboxItemProps = {
option: menuInputOptionT
className?: string
checked?: boolean
onCheckedChange?: (checked: boolean) => void
indicatorAt?: indicatorAtT
}
function CheckboxItem({
option,
className,
checked = false,
indicatorAt,
onCheckedChange = () => { },
}: checkboxItemProps) {
const value = getValue(option)
if (isSeparator(value)) return <ContextMenuSeparator className={cn(className)} />
const label = getLabel(option)
const disabled = (option as any)?.disabled
return (
<ContextMenuCheckboxItem
checked={checked}
disabled={disabled}
className={cn(className)}
indicatorAt={indicatorAt}
onCheckedChange={onCheckedChange}
>
{label}
</ContextMenuCheckboxItem>
)
}
type radioItemProps = {
option: menuInputOptionT
className?: string
indicatorAt?: indicatorAtT
}
function RadioItem({ option, className, indicatorAt }: radioItemProps) {
const value = getValue(option)
if (isSeparator(value)) return <ContextMenuSeparator className={cn(className)} />
const label = getLabel(option)
const disabled = (option as any)?.disabled
return (
<ContextMenuRadioItem
value={`${value}`}
disabled={disabled}
className={cn(className)}
indicatorAt={indicatorAt}
>
{label}
</ContextMenuRadioItem>
)
}
type SubMenuProps = commonSubMenuT & {
submenu: subMenuT
onSelect?: (value: allowedPrimitiveT) => void
}
function SubMenu({ submenu, itemCls, groupCls, groupLabelCls, onSelect }: SubMenuProps) {
return (
<ContextMenuSub>
<ContextMenuSubTrigger className={cn(submenu.triggerCls)}>
{submenu.submenu}
</ContextMenuSubTrigger>
<ContextMenuSubContent className={cn(submenu.contentCls)}>
{submenu.options.map((option, i) => {
if (isGroupMenu(option)) {
return (
<ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
<ContextMenuLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
option.groupLabelCls,
)}
>
{option.group}
</ContextMenuLabel>
{option.options.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</ContextMenuGroup>
)
}
if (isSubMenu(option)) {
return (
<SubMenu
key={getKey(option, i)}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(option, i)}
option={option}
className={itemCls}
onSelect={() => onSelect?.(getValue(option))}
/>
)
})}
</ContextMenuSubContent>
</ContextMenuSub>
)
}
type CheckboxSubMenuProps = commonSubMenuT &
commonCheckboxProps & {
submenu: inputSubMenuT
}
function CheckboxSubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
checked = [],
indicatorAt,
onCheckedChange = () => { },
}: CheckboxSubMenuProps) {
return (
<ContextMenuSub>
<ContextMenuSubTrigger className={cn(submenu.triggerCls)}>
{submenu.submenu}
</ContextMenuSubTrigger>
<ContextMenuSubContent className={cn(submenu.contentCls)}>
{submenu.options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
<ContextMenuLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
option.groupLabelCls,
)}
>
{option.group}
</ContextMenuLabel>
{option.options.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
option={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={checked => onCheckedChange?.(v, checked)}
/>
)
})}
</ContextMenuGroup>
)
}
if (isInputSubMenu(option)) {
return (
<CheckboxSubMenu
key={option.submenu}
submenu={option}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(option)
return (
<CheckboxItem
key={getKey(option, i)}
option={option}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={checked => onCheckedChange?.(v, checked)}
/>
)
})}
</ContextMenuSubContent>
</ContextMenuSub>
)
}
type RadioSubMenuProps = commonSubMenuT &
commonRadioProps & {
submenu: inputSubMenuT
}
function RadioSubMenu({
submenu,
itemCls,
groupCls,
groupLabelCls,
value = '',
indicatorAt,
onValueChange = () => { },
}: RadioSubMenuProps) {
return (
<ContextMenuSub>
<ContextMenuSubTrigger className={cn(submenu.triggerCls)}>
{submenu.submenu}
</ContextMenuSubTrigger>
<ContextMenuSubContent className={cn(submenu.contentCls)}>
<ContextMenuRadioGroup value={`${value}`} onValueChange={onValueChange}>
{submenu.options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
<ContextMenuLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
option.groupLabelCls,
)}
>
{option.group}
</ContextMenuLabel>
{option.options.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</ContextMenuGroup>
)
}
if (isInputSubMenu(option)) {
return (
<RadioSubMenu
key={option.submenu}
value={value}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={onValueChange}
/>
)
}
return (
<RadioItem
key={getKey(option, i)}
option={option}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</ContextMenuRadioGroup>
</ContextMenuSubContent>
</ContextMenuSub>
)
}
type ContextWrapperProps = commonPropsT & {
options: menuOptionsT
}
function ContextWrapper({
children,
options,
itemCls,
groupCls,
groupLabelCls,
contentProps,
onSelect,
...props
}: ContextWrapperProps) {
return (
<ContextMenu {...props}>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent {...contentProps}>
{options.map((option, i) => {
if (isGroupMenu(option)) {
return (
<ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
<ContextMenuLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
option.groupLabelCls,
)}
>
{option.group}
</ContextMenuLabel>
{option.options.map((grOpt, j) => (
<Item
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
onSelect={() => onSelect?.(getValue(grOpt))}
/>
))}
</ContextMenuGroup>
)
}
if (isSubMenu(option)) {
return (
<SubMenu
key={option.submenu}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
groupLabelCls={groupLabelCls}
onSelect={onSelect}
/>
)
}
return (
<Item
key={getKey(option, i)}
option={option}
className={itemCls}
onSelect={() => onSelect?.(getValue(option))}
/>
)
})}
</ContextMenuContent>
</ContextMenu>
)
}
type ContextCheckboxWrapperProps = commonPropsT &
commonCheckboxProps & {
options: menuInputOptionsT
}
function ContextCheckboxWrapper({
children,
options,
contentProps,
itemCls,
groupCls,
groupLabelCls,
checked: o_checked,
onCheckedChange: o_onCheckedChange,
indicatorAt,
...props
}: ContextCheckboxWrapperProps) {
const [i_checked, setIChecked] = useState<allowedPrimitiveT[]>([])
function i_Checked(v: allowedPrimitiveT, c: boolean) {
setIChecked(prev => (!c ? prev.filter(p => p !== v) : [...prev, v]))
}
const checked = o_checked ?? i_checked
const onCheckedChange = o_onCheckedChange ?? i_Checked
return (
<ContextMenu {...props}>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent {...contentProps}>
{options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
<ContextMenuLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
option.groupLabelCls,
)}
>
{option.group}
</ContextMenuLabel>
{option.options.map((grOpt, j) => {
const v = getValue(grOpt)
return (
<CheckboxItem
key={getKey(grOpt, j)}
option={grOpt}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={checked => onCheckedChange?.(v, checked)}
/>
)
})}
</ContextMenuGroup>
)
}
if (isInputSubMenu(option)) {
return (
<CheckboxSubMenu
key={option.submenu}
submenu={option}
checked={checked}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onCheckedChange={onCheckedChange}
/>
)
}
const v = getValue(option)
return (
<CheckboxItem
key={getKey(option, i)}
option={option}
checked={checked.includes(v)}
className={itemCls}
indicatorAt={indicatorAt}
onCheckedChange={checked => onCheckedChange?.(v, checked)}
/>
)
})}
</ContextMenuContent>
</ContextMenu>
)
}
type ContextRadioWrapperProps = commonPropsT &
commonRadioProps & {
options: menuInputOptionsT
}
function ContextRadioWrapper({
children,
options,
itemCls,
groupCls,
groupLabelCls,
contentProps,
value: o_value,
onValueChange: o_onValueChange,
indicatorAt,
...props
}: ContextRadioWrapperProps) {
const [i_value, setIValue] = useState<allowedPrimitiveT>('')
const value = o_value ?? i_value
const onValueChange = o_onValueChange ?? setIValue
return (
<ContextMenu {...props}>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent {...contentProps}>
<ContextMenuRadioGroup
value={`${value}`}
onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
>
{options.map((option, i) => {
if (isInputGroupMenu(option)) {
return (
<ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
<ContextMenuLabel
className={cn(
'pb-0.5 text-xs text-muted-foreground font-normal',
groupLabelCls,
option.groupLabelCls,
)}
>
{option.group}
</ContextMenuLabel>
{option.options.map((grOpt, j) => (
<RadioItem
key={getKey(grOpt, j)}
option={grOpt}
className={itemCls}
indicatorAt={indicatorAt}
/>
))}
</ContextMenuGroup>
)
}
if (isInputSubMenu(option)) {
return (
<RadioSubMenu
key={option.submenu}
value={value}
submenu={option}
itemCls={itemCls}
groupCls={groupCls}
indicatorAt={indicatorAt}
groupLabelCls={groupLabelCls}
onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
/>
)
}
return (
<RadioItem
key={getKey(option, i)}
option={option}
className={itemCls}
indicatorAt={indicatorAt}
/>
)
})}
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
)
}
export { ContextWrapper, ContextCheckboxWrapper, ContextRadioWrapper }
Usage
Basic
import { ContextWrapper } from "@/components/ui/context-menu-wrapper"
<ContextWrapper options={["Cut", "Copy", "Paste"]}>
<div className="border border-dashed rounded-lg p-8">
Right-click here
</div>
</ContextWrapper>With shortcuts and destructive item
<ContextWrapper
options={[
{ label: "Edit", value: "edit", shortcut: "⌘E" },
{ label: "Delete", value: "delete", variant: "destructive", shortcut: "⌘⌫" },
]}
onSelect={v => console.log(v)}
>
<div>Right-click here</div>
</ContextWrapper>Checkbox
const [checked, setChecked] = useState<allowedPrimitiveT[]>([])
<ContextCheckboxWrapper
options={["Auto-save", "Spellcheck", "Dark mode"]}
checked={checked}
onCheckedChange={(v, c) =>
setChecked(prev => (c ? [...prev, v] : prev.filter(x => x !== v)))
}
>
<div>Right-click here</div>
</ContextCheckboxWrapper>Radio
const [val, setVal] = useState<allowedPrimitiveT>("Light")
<ContextRadioWrapper
options={["Light", "Dark", "System"]}
value={val}
onValueChange={setVal}
>
<div>Right-click here</div>
</ContextRadioWrapper>Indicator position
<ContextCheckboxWrapper options={options} indicatorAt="right">
<div>Right-click here</div>
</ContextCheckboxWrapper>Groups, submenus, separators
const options: menuOptionsT = [
{ label: "Cut", value: "cut", shortcut: "⌘X" },
{ label: "Copy", value: "copy", shortcut: "⌘C" },
"---",
{
group: "Actions",
options: ["Save", "Export"],
},
{
submenu: "Share",
options: ["Email", "Link"],
},
]Reference
ContextWrapper
Prop
Type
ContextCheckboxWrapper
Prop
Type
ContextRadioWrapper
Prop
Type
Related Components
- Base UI Context Menu
- Shadcn Context Menu
- Menu — same options API, triggered by click