Kosorikōsori alpha

Context Menu

Displays a menu to the user — such as a set of actions or functions — triggered by a button.

Right click here

Installation

Install the primitive

Install the @radix-ui/react-context-menu package.

npm install @radix-ui/react-context-menu

Copy-paste the component

Copy and paste the component code in a .tsx file.

'use client';
 
import { forwardRef } from 'react';
import {
  CheckboxItem,
  Content,
  Group,
  Item,
  ItemIndicator,
  Label,
  Portal,
  RadioGroup,
  RadioItem,
  Root,
  Separator,
  Sub,
  SubContent,
  SubTrigger,
  Trigger,
} from '@radix-ui/react-context-menu';
import {
  CheckIcon,
  ChevronRightIcon,
  DotFilledIcon,
} from '@radix-ui/react-icons';
import { clsx } from 'clsx/lite';
import { tv } from 'tailwind-variants';
 
const contextMenuStyles = tv({
  slots: {
    content: clsx(
      'z-50 min-w-[8rem] overflow-hidden rounded-lg border bg-grey-base p-1 shadow-md',
      'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
      'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
      'data-[side=top]:slide-in-from-bottom-2',
      'data-[side=right]:slide-in-from-left-2',
      'data-[side=bottom]:slide-in-from-top-2',
      'data-[side=left]:slide-in-from-right-2',
    ),
    item: clsx(
      'group relative flex h-8 cursor-pointer select-none items-center rounded-md px-2 text-sm outline-none transition-colors',
      'focus:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    label:
      'flex h-8 select-none items-center px-2 text-xs font-medium text-grey-text',
    checkboxItem: clsx(
      'group relative flex h-8 cursor-pointer select-none items-center rounded-md pl-8 pr-2 text-sm outline-none transition-colors duration-200',
      'focus:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    checkboxItemIndicator:
      'absolute left-2 flex size-3.5 items-center justify-center',
    checkboxItemIcon: 'size-4',
    radioItem: clsx(
      'relative flex h-8 cursor-pointer select-none items-center rounded-md pl-8 pr-2 text-sm outline-none transition-colors duration-200',
      'focus:bg-primary-bg-hover',
      'active:bg-primary-bg-active',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    radioItemIndicator:
      'absolute left-2 flex size-3.5 items-center justify-center',
    radioItemIcon: 'size-4',
    separator: '-mx-2 my-1 h-px bg-grey-line',
    subTrigger: clsx(
      'group flex h-8 cursor-pointer select-none items-center rounded-md px-2 text-sm outline-none transition-colors',
      'focus:bg-primary-bg-hover',
      'data-[state=open]:bg-primary-bg-hover',
      'data-[disabled]:cursor-not-allowed data-[disabled]:text-grey-solid',
    ),
    subTriggerIcon: clsx(
      'ml-auto size-4',
      'group-data-[disabled]:text-grey-solid',
    ),
    subContent: clsx(
      'z-50 min-w-[8rem] overflow-hidden rounded-lg border border-grey-line bg-grey-base p-1 shadow-md',
      'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
      'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
      'data-[side=top]:slide-in-from-bottom-2',
      'data-[side=right]:slide-in-from-left-2',
      'data-[side=bottom]:slide-in-from-top-2',
      'data-[side=left]:slide-in-from-right-2',
    ),
    shortcut: clsx(
      'ml-auto text-xs tracking-widest text-grey-text',
      'group-data-[disabled]:text-grey-solid',
    ),
  },
  variants: {
    inset: {
      true: {
        item: 'pl-8',
        label: 'pl-8',
        subTrigger: 'pl-8',
      },
    },
  },
});
 
const {
  content,
  item,
  label,
  checkboxItem,
  checkboxItemIndicator,
  checkboxItemIcon,
  radioItem,
  radioItemIndicator,
  radioItemIcon,
  separator,
  subTrigger,
  subTriggerIcon,
  subContent,
  shortcut,
} = contextMenuStyles();
 
/**
 * ContextMenu component that provides a context menu for user interactions.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Root>} props - The props for the ContextMenu component.
 *
 * @example
 * <ContextMenu>
 *   <ContextMenuTrigger>Right click</ContextMenuTrigger>
 *   <ContextMenuContent>
 *     <ContextMenuItem>Profile</ContextMenuItem>
 *     <ContextMenuItem>Billing</ContextMenuItem>
 *     <ContextMenuItem>Team</ContextMenuItem>
 *     <ContextMenuItem>Subscription</ContextMenuItem>
 *   </ContextMenuContent>
 * </ContextMenu>
 *
 * @see {@link https://dub.sh/ui-context-menu Context Menu Docs} for further information.
 */
export const ContextMenu = Root;
 
/**
 * ContextMenuTrigger component that serves as the trigger for the ContextMenu.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Trigger>} props - The props for the ContextMenuTrigger component.
 *
 * @example
 * <ContextMenuTrigger>Right click</ContextMenuTrigger>
 */
export const ContextMenuTrigger = Trigger;
 
/**
 * ContextMenuPortal component that renders the ContextMenu in a portal.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Portal>} props - The props for the ContextMenuPortal component.
 */
export const ContextMenuPortal = Portal;
 
type ContextMenuContentRef = React.ElementRef<typeof Content>;
type ContextMenuContentProps = React.ComponentPropsWithoutRef<typeof Content>;
 
/**
 * ContextMenuContent component that wraps the content of the ContextMenu.
 *
 * @param {ContextMenuContentProps} props - The props for the ContextMenuContent component.
 *
 * @example
 * <ContextMenuContent>
 *   <ContextMenuItem>Profile</ContextMenuItem>
 * </ContextMenuContent>
 */
export const ContextMenuContent = forwardRef<
  ContextMenuContentRef,
  ContextMenuContentProps
>(({ className, ...props }, ref) => (
  <Portal>
    <Content ref={ref} className={content({ className })} {...props} />
  </Portal>
));
 
ContextMenuContent.displayName = Content.displayName;
 
type ContextMenuItemRef = React.ElementRef<typeof Item>;
type ContextMenuItemProps = React.ComponentPropsWithoutRef<typeof Item> & {
  inset?: boolean;
};
 
/**
 * ContextMenuItem component that represents a single item in the ContextMenu.
 *
 * @param {ContextMenuItemProps} props - The props for the ContextMenuItem component.
 * @param {boolean} [props.inset] - Whether the item is inset.
 *
 * @example
 * <ContextMenuItem>Profile</ContextMenuItem>
 */
export const ContextMenuItem = forwardRef<
  ContextMenuItemRef,
  ContextMenuItemProps
>(({ className, inset, ...props }, ref) => (
  <Item ref={ref} className={item({ className, inset })} {...props} />
));
 
ContextMenuItem.displayName = Item.displayName;
 
/**
 * ContextMenuGroup component that groups related command items.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Group>} props - The props for the ContextMenuGroup component.
 *
 * @example
 * <ContextMenuGroup>...</ContextMenuGroup>
 */
export const ContextMenuGroup = Group;
 
type ContextMenuLabelRef = React.ElementRef<typeof Label>;
type ContextMenuLabelProps = React.ComponentPropsWithoutRef<typeof Label> & {
  inset?: boolean;
};
 
/**
 * ContextMenuLabel component that displays a label for a group of items in the ContextMenu.
 *
 * @param {ContextMenuLabelProps} props - The props for the ContextMenuLabel component.
 * @param {boolean} [props.inset] - Whether the label is inset.
 *
 * @example
 * <ContextMenuLabel>Settings</ContextMenuLabel>
 */
export const ContextMenuLabel = forwardRef<
  ContextMenuLabelRef,
  ContextMenuLabelProps
>(({ className, inset, ...props }, ref) => (
  <Label ref={ref} className={label({ className, inset })} {...props} />
));
 
ContextMenuLabel.displayName = Label.displayName;
 
type ContextMenuCheckboxItemRef = React.ElementRef<typeof CheckboxItem>;
type ContextMenuCheckboxItemProps = React.ComponentPropsWithoutRef<
  typeof CheckboxItem
>;
 
/**
 * ContextMenuCheckboxItem component that represents a checkbox item in the ContextMenu.
 *
 * @param {ContextMenuCheckboxItemProps} props - The props for the ContextMenuCheckboxItem component.
 *
 * @example
 * <ContextMenuCheckboxItem checked={true}>Enable Notifications</ContextMenuCheckboxItem>
 */
export const ContextMenuCheckboxItem = forwardRef<
  ContextMenuCheckboxItemRef,
  ContextMenuCheckboxItemProps
>(({ className, children, checked, ...props }, ref) => (
  <CheckboxItem
    ref={ref}
    checked={checked}
    className={checkboxItem({ className })}
    {...props}
  >
    <span className={checkboxItemIndicator()}>
      <ItemIndicator>
        <CheckIcon className={checkboxItemIcon()} />
      </ItemIndicator>
    </span>
    {children}
  </CheckboxItem>
));
 
ContextMenuCheckboxItem.displayName = CheckboxItem.displayName;
 
/**
 * ContextMenuRadioGroup component that represents a group of radio items in the ContextMenu.
 *
 * @param {React.ComponentPropsWithoutRef<typeof RadioGroup>} props - The props for the ContextMenuRadioGroup component.
 *
 * @example
 * <ContextMenuRadioGroup>...</ContextMenuRadioGroup>
 */
export const ContextMenuRadioGroup = RadioGroup;
 
type ContextMenuRadioItemRef = React.ElementRef<typeof RadioItem>;
type ContextMenuRadioItemProps = React.ComponentPropsWithoutRef<
  typeof RadioItem
>;
 
/**
 * ContextMenuRadioItem component that represents a radio item in the ContextMenu.
 *
 * @param {ContextMenuRadioItemProps} props - The props for the ContextMenuRadioItem component.
 *
 * @example
 * <ContextMenuRadioItem>Option 1</ContextMenuRadioItem>
 */
export const ContextMenuRadioItem = forwardRef<
  ContextMenuRadioItemRef,
  ContextMenuRadioItemProps
>(({ className, children, ...props }, ref) => (
  <RadioItem ref={ref} className={radioItem({ className })} {...props}>
    <span className={radioItemIndicator()}>
      <ItemIndicator>
        <DotFilledIcon className={radioItemIcon()} />
      </ItemIndicator>
    </span>
    {children}
  </RadioItem>
));
 
ContextMenuRadioItem.displayName = RadioItem.displayName;
 
type ContextMenuSeparatorRef = React.ElementRef<typeof Separator>;
type ContextMenuSeparatorProps = React.ComponentPropsWithoutRef<
  typeof Separator
>;
 
/**
 * ContextMenuSeparator component that visually separates items in the ContextMenu.
 *
 * @param {ContextMenuSeparatorProps} props - The props for the ContextMenuSeparator component.
 *
 * @example
 * <ContextMenuSeparator />
 */
export const ContextMenuSeparator = forwardRef<
  ContextMenuSeparatorRef,
  ContextMenuSeparatorProps
>(({ className, ...props }, ref) => (
  <Separator ref={ref} className={separator({ className })} {...props} />
));
 
ContextMenuSeparator.displayName = Separator.displayName;
 
/**
 * ContextMenuSub component that represents a sub-menu in the ContextMenu.
 *
 * @param {React.ComponentPropsWithoutRef<typeof Sub>} props - The props for the ContextMenuSub component.
 *
 * @example
 * <ContextMenuSub>...</ContextMenuSub>
 */
export const ContextMenuSub = Sub;
 
type ContextMenuSubTriggerRef = React.ElementRef<typeof SubTrigger>;
type ContextMenuSubTriggerProps = React.ComponentPropsWithoutRef<
  typeof SubTrigger
> & {
  inset?: boolean;
};
 
/**
 * ContextMenuSubTrigger component that serves as the trigger for a sub-menu in the ContextMenu.
 *
 * @param {ContextMenuSubTriggerProps} props - The props for the ContextMenuSubTrigger component.
 * @param {boolean} [props.inset] - Whether the trigger is inset.
 *
 * @example
 * <ContextMenuSubTrigger>More Options</ContextMenuSubTrigger>
 */
export const ContextMenuSubTrigger = forwardRef<
  ContextMenuSubTriggerRef,
  ContextMenuSubTriggerProps
>(({ className, inset, children, ...props }, ref) => (
  <SubTrigger ref={ref} className={subTrigger({ className, inset })} {...props}>
    {children}
    <ChevronRightIcon className={subTriggerIcon()} />
  </SubTrigger>
));
 
ContextMenuSubTrigger.displayName = SubTrigger.displayName;
 
type ContextMenuSubContentRef = React.ElementRef<typeof SubContent>;
type ContextMenuSubContentProps = React.ComponentPropsWithoutRef<
  typeof SubContent
>;
 
/**
 * ContextMenuSubContent component that wraps the content of a sub-menu in the ContextMenu.
 *
 * @param {ContextMenuSubContentProps} props - The props for the ContextMenuSubContent component.
 *
 * @example
 * <ContextMenuSubContent>...</ContextMenuSubContent>
 */
export const ContextMenuSubContent = forwardRef<
  ContextMenuSubContentRef,
  ContextMenuSubContentProps
>(({ className, ...props }, ref) => (
  <SubContent ref={ref} className={subContent({ className })} {...props} />
));
 
ContextMenuSubContent.displayName = SubContent.displayName;
 
type ContextMenuShortcutProps = React.HTMLAttributes<HTMLSpanElement>;
 
/**
 * ContextMenuShortcut component that displays a keyboard shortcut for a command item.
 *
 * @param {ContextMenuShortcutProps} props - The props for the ContextMenuShortcut component.
 *
 * @example
 * <ContextMenuShortcut>⌘K</ContextMenuShortcut>
 */
export const ContextMenuShortcut = ({
  className,
  ...props
}: ContextMenuShortcutProps) => (
  <span className={shortcut({ className })} {...props} />
);
 
ContextMenuShortcut.displayName = 'ContextMenuShortcut';

Update import paths

Update the @kosori/ui import paths to fit your project structure, for example, using ~/components/ui.

Usage

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from '~/components/ui/context-menu';
<ContextMenu>
  <ContextMenuTrigger>Right click</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>Profile</ContextMenuItem>
    <ContextMenuItem>Billing</ContextMenuItem>
    <ContextMenuItem>Team</ContextMenuItem>
    <ContextMenuItem>Subscription</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

On this page