rapidUI

Context Menu

Right-click menu with items, checkboxes, radio items and submenus.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/context-menu.json

Installs to @/components/ui/context-menu.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0 lucide-react@^1.48.0

Copy the source into your project:

components/ui/context-menu.tsx
'use client'

import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu'
import { CheckIcon, ChevronRightIcon } from 'lucide-react'
import * as React from '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="isolate z-50 outline-none"
				align={align}
				alignOffset={alignOffset}
				side={side}
				sideOffset={sideOffset}
			>
				<ContextMenuPrimitive.Popup
					data-slot="context-menu-content"
					className={cn(
						'z-50 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-border 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-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 [&_svg:not([class*='text-'])]:text-muted-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" aria-hidden="true" />
		</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,
	...props
}: ContextMenuPrimitive.CheckboxItem.Props & {
	inset?: boolean
}) {
	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 pr-8 pl-1.5 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,
			)}
			checked={checked}
			{...props}
		>
			<span className="pointer-events-none absolute right-2">
				<ContextMenuPrimitive.CheckboxItemIndicator>
					<CheckIcon aria-hidden="true" />
				</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,
	...props
}: ContextMenuPrimitive.RadioItem.Props & {
	inset?: boolean
}) {
	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 pr-8 pl-1.5 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,
			)}
			{...props}
		>
			<span className="pointer-events-none absolute right-2">
				<ContextMenuPrimitive.RadioItemIndicator>
					<CheckIcon aria-hidden="true" />
				</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', className)}
			{...props}
		/>
	)
}

export {
	ContextMenu,
	ContextMenuTrigger,
	ContextMenuContent,
	ContextMenuItem,
	ContextMenuCheckboxItem,
	ContextMenuRadioItem,
	ContextMenuLabel,
	ContextMenuSeparator,
	ContextMenuShortcut,
	ContextMenuGroup,
	ContextMenuPortal,
	ContextMenuSub,
	ContextMenuSubContent,
	ContextMenuSubTrigger,
	ContextMenuRadioGroup,
}

A context menu component that displays a menu when right-clicking on a trigger element. Built with accessibility and keyboard navigation in mind using composable primitives.

Features

  • Right-click activation with customizable trigger areas
  • Composable primitive-based architecture for maximum flexibility
  • Support for menu items, checkboxes, radio buttons, and submenus
  • Keyboard shortcuts display with ContextMenuShortcut
  • Grouping and labeling with ContextMenuGroup and ContextMenuLabel
  • Separators for visual organization
  • Inset support for hierarchical menu layouts
  • Disabled states for menu items
  • Destructive item variant (variant="destructive")
  • Icons inside items are sized automatically (size-4), no extra classes needed
  • Checkbox and radio items show a check indicator on the right
  • Keyboard navigation inside the open menu
  • Screen reader friendly
  • Built on top of Base UI Context Menu

A context menu opens on right-click or long-press only, so keyboard and most screen reader users can't open it. Every action in a context menu must also be available from a visible control, such as a DropdownMenu "More actions" button.

Usage

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioItem,
  ContextMenuRadioGroup,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuGroup,
  ContextMenuSub,
  ContextMenuSubTrigger,
  ContextMenuSubContent,
} from "@/components/ui/context-menu"

// Basic usage
<ContextMenu>
  <ContextMenuTrigger className="rounded-xl border border-dashed p-4 data-popup-open:border-ring">
    Site plan, Storgatan 14.pdf
  </ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>Open</ContextMenuItem>
    <ContextMenuItem>Download</ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem>Show in case file</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

// With shortcuts
<ContextMenu>
  <ContextMenuTrigger>Right click target</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>
      Copy case number
      <ContextMenuShortcut>⌘C</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuItem>
      Print summary
      <ContextMenuShortcut>⌘P</ContextMenuShortcut>
    </ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

// With checkboxes and radio items
const [showClosed, setShowClosed] = useState(true)
const [sortBy, setSortBy] = useState('case-number')

<ContextMenu>
  <ContextMenuTrigger>Right click target</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuGroup>
      <ContextMenuLabel>View</ContextMenuLabel>
      <ContextMenuCheckboxItem checked={showClosed} onCheckedChange={setShowClosed}>
        Show closed cases
      </ContextMenuCheckboxItem>
    </ContextMenuGroup>
    <ContextMenuSeparator />
    <ContextMenuRadioGroup value={sortBy} onValueChange={setSortBy}>
      <ContextMenuLabel>Sort by</ContextMenuLabel>
      <ContextMenuRadioItem value="case-number">Case number</ContextMenuRadioItem>
      <ContextMenuRadioItem value="deadline">Deadline</ContextMenuRadioItem>
    </ContextMenuRadioGroup>
  </ContextMenuContent>
</ContextMenu>

// With submenus
<ContextMenu>
  <ContextMenuTrigger>Right click target</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>Open case</ContextMenuItem>
    <ContextMenuSub>
      <ContextMenuSubTrigger>Assign to</ContextMenuSubTrigger>
      <ContextMenuSubContent>
        <ContextMenuItem>Ingrid Hansen</ContextMenuItem>
        <ContextMenuItem>Mikko Virtanen</ContextMenuItem>
      </ContextMenuSubContent>
    </ContextMenuSub>
  </ContextMenuContent>
</ContextMenu>

Examples

Default

Right-click a case document for a basic menu.

With Shortcuts

A case card whose menu shows keyboard shortcuts alongside the items.

With Checkboxes

Checkbox items and a radio group that change how the case list is shown. Try toggling them.

With Submenu

Nested submenus for assigning a case and changing its status.

With Groups

Items grouped with labels and separators on a council meeting.

With Inset Items

Inset items line up with items that have an icon or indicator, under a label.

With Icons and Destructive Item

A document menu with leading icons, a disabled item and a destructive action.

Disabled

A context menu with disabled states for item, checkbox item, radiogroup item and item in submenu.

Props

ContextMenu (Root)

  • Extends Base UI ContextMenu.Root props
  • No additional custom props

ContextMenuTrigger

  • Extends Base UI ContextMenu.Trigger props
  • The area that opens the context menu on right-click or long-press. It renders a <div>; use render to render your own element, e.g. render={<FileTile />}
  • While the menu is open the trigger has data-popup-open

ContextMenuItem

  • variant?: 'default' | 'destructive' - Visual style of the item (default 'default')
  • inset?: boolean - Whether to add left padding for alignment with items that have an icon
  • disabled?: boolean - Whether the item is disabled
  • Extends Base UI Menu.Item props (onClick, closeOnClick, label)

ContextMenuCheckboxItem

  • checked?: boolean - The checked state of the checkbox
  • onCheckedChange?: (checked: boolean) => void - Callback when checked state changes
  • inset?: boolean - Whether to add left padding for alignment
  • Extends Base UI Menu.CheckboxItem props. Checkbox items keep the menu open unless closeOnClick is set

ContextMenuRadioItem

  • value: string - The value of the radio item
  • inset?: boolean - Whether to add left padding for alignment
  • Extends Base UI Menu.RadioItem props

ContextMenuRadioGroup

  • value?: string - The controlled value of the radio group
  • onValueChange?: (value: string) => void - Callback when value changes
  • Extends Base UI Menu.RadioGroup props

ContextMenuLabel

  • inset?: boolean - Whether to add left padding for alignment with items
  • Extends Base UI Menu.GroupLabel props. A label names its group, so place it inside a ContextMenuGroup or ContextMenuRadioGroup

ContextMenuSubTrigger

  • inset?: boolean - Whether to add left padding for alignment
  • Extends Base UI Menu.SubmenuTrigger props

Accessibility

The Context Menu component follows WCAG 2.1 guidelines and provides:

  • Proper ARIA roles and attributes for menu structure
  • Keyboard navigation support:
    • ↑/↓: Navigate through menu items
    • →: Open submenu or move to parent menu
    • ←: Close submenu or move to child menu
    • Enter/Space: Activate menu item
    • Esc: Close menu
  • Focus management with proper focus trapping
  • Screen reader announcements for menu state changes
  • Support for disabled states
  • Clear visual focus indicators

Best Practices

  1. Content Organization

    • Use ContextMenuGroup and ContextMenuLabel to organize related items
    • Use ContextMenuSeparator to visually separate different sections
    • Keep menu hierarchies shallow (max 2-3 levels) for better usability
  2. Trigger Areas

    • Make trigger areas large enough for easy right-clicking
    • Provide visual feedback to indicate interactive areas
    • Consider touch device users who may need alternative access methods
  3. Menu Items

    • Use clear, descriptive labels for menu items
    • Show keyboard shortcuts when available using ContextMenuShortcut
    • Disable items that are not currently available rather than hiding them
  4. State Management

    • Use controlled components for checkbox and radio items when state needs to persist
    • Provide immediate visual feedback for state changes
    • Consider grouping related stateful items together
  5. Accessibility

    • Test with keyboard navigation thoroughly
    • Verify screen reader compatibility
    • Ensure sufficient color contrast for all states
    • Use proper semantic markup with labels and descriptions
  • Dropdown Menu
  • Menubar
  • Command
  • Popover