Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/context-menu.jsonInstalls to @/components/ui/context-menu.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 lucide-react@^1.48.0Copy the source into your project:
'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
ContextMenuGroupandContextMenuLabel - 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>; userenderto 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 icondisabled?: boolean- Whether the item is disabled- Extends Base UI Menu.Item props (
onClick,closeOnClick,label)
ContextMenuCheckboxItem
checked?: boolean- The checked state of the checkboxonCheckedChange?: (checked: boolean) => void- Callback when checked state changesinset?: boolean- Whether to add left padding for alignment- Extends Base UI Menu.CheckboxItem props. Checkbox items keep the menu open unless
closeOnClickis set
ContextMenuRadioItem
value: string- The value of the radio iteminset?: boolean- Whether to add left padding for alignment- Extends Base UI Menu.RadioItem props
ContextMenuRadioGroup
value?: string- The controlled value of the radio grouponValueChange?: (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
ContextMenuGrouporContextMenuRadioGroup
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 menuEnter/Space: Activate menu itemEsc: 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
-
Content Organization
- Use
ContextMenuGroupandContextMenuLabelto organize related items - Use
ContextMenuSeparatorto visually separate different sections - Keep menu hierarchies shallow (max 2-3 levels) for better usability
- Use
-
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
-
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
-
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
-
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
Related Components
- Dropdown Menu
- Menubar
- Command
- Popover