Primitives
Context Menu
Pointer-positioned right-click and long-press menu with nested actions and selection controls.
Composition
Contextual actions and selection
Content includes its portal and positioner.
- <ContextMenu>
- <ContextMenuTrigger />
- <ContextMenuContent>
- <ContextMenuGroup>
- <ContextMenuLabel />
- <ContextMenuItem>
- <ContextMenuShortcut />
- <ContextMenuCheckboxItem />
- <ContextMenuSeparator />
- <ContextMenuRadioGroup>
- <ContextMenuRadioItem />
- <ContextMenuSub>
- <ContextMenuSubTrigger />
- <ContextMenuSubContent>
- <ContextMenuItem />
- <ContextMenuGroup>
Custom portal content
Use the portal only when supplying your own popup instead of ContextMenuContent.
- <ContextMenu>
- <ContextMenuTrigger />
- <ContextMenuPortal>
- custom positioned popup
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/context-menu.tsx.
npx shadcn@latest add https://control-ui.dev/r/context-menu.jsonDependencies
Surface variants
src/registry/sources/control-ui/surface-variants.tsSupportRaw code
Primary installed source
"use client";import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";import type { ComponentProps, CSSProperties, MouseEvent, ReactNode } from "react";import type { ControlledChoice, OpenChangeEventDetails } from "@/components/control-ui/control-props";import type { PopupKnobStyle } from "@/components/control-ui/knob-contracts/popup-knobs";import { cn } from "@/components/control-ui/lib/cn";import { controlEffectsAttribute } from "@/components/control-ui/skin";import { useSkin } from "@/components/control-ui/skin-provider";import { popupItemStructureClasses } from "@/components/control-ui/surface-variants";export type ContextMenuProps = { children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean, eventDetails: OpenChangeEventDetails) => void;};export type ContextMenuTriggerProps = ComponentProps<"div"> & { style?: CSSProperties & PopupKnobStyle };export type ContextMenuGroupProps = ComponentProps<"div"> & { style?: CSSProperties & PopupKnobStyle };export type ContextMenuContentProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };export type ContextMenuItemProps = Omit<Omit<ComponentProps<"div">, "onClick">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { disabled?: boolean; inset?: boolean; onClick?: (event: MouseEvent) => void;};export type ContextMenuCheckboxItemProps = Omit<Omit<ComponentProps<"div">, "onClick">, "style"> & { style?: CSSProperties & PopupKnobStyle;} & { checked?: boolean; onCheckedChange?: (checked: boolean) => void; disabled?: boolean;};export type ContextMenuRadioGroupProps<TValue extends string = string> = ComponentProps<"div"> & Omit<ControlledChoice<TValue>, "defaultValue"> & { style?: CSSProperties & PopupKnobStyle };export type ContextMenuRadioItemProps = Omit<Omit<ComponentProps<"div">, "onClick">, "style"> & { style?: CSSProperties & PopupKnobStyle;} & { value: string; disabled?: boolean;};export type ContextMenuLabelProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { inset?: boolean;};export type ContextMenuSeparatorProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };export type ContextMenuShortcutProps = Omit<ComponentProps<"span">, "style"> & { style?: CSSProperties & PopupKnobStyle };export type ContextMenuSubProps = { children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean, eventDetails: OpenChangeEventDetails) => void;};export type ContextMenuSubTriggerProps = Omit<Omit<ComponentProps<"div">, "onClick">, "style"> & { style?: CSSProperties & PopupKnobStyle;} & { disabled?: boolean; inset?: boolean;};export type ContextMenuSubContentProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };const popupClasses = "min-w-[10rem]";const itemClasses = cn("group/cmi relative", popupItemStructureClasses);export function ContextMenu(props: ContextMenuProps) { return <ContextMenuPrimitive.Root {...props} />;}export function ContextMenuTrigger({ className, ...props }: ContextMenuTriggerProps) { return ( <ContextMenuPrimitive.Trigger data-control-ui="context-menu" data-control-family="popup" data-popup-kind="context-menu" data-slot="trigger" className={className} {...props} /> );}export function ContextMenuPortal(props: ComponentProps<typeof ContextMenuPrimitive.Portal>) { return <ContextMenuPrimitive.Portal {...props} />;}export function ContextMenuGroup({ className, ...props }: ContextMenuGroupProps) { return ( <ContextMenuPrimitive.Group data-control-ui="context-menu" data-control-family="popup" data-popup-kind="context-menu" data-slot="group" className={className} {...props} /> );}export function ContextMenuContent({ className, children, ...props }: ContextMenuContentProps) { const skin = useSkin(); return ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Positioner data-skin={skin.id} data-effects={controlEffectsAttribute(skin.effects)} className="z-(--z-popup)"> <ContextMenuPrimitive.Popup data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="content" data-surface="floating" data-control-family="popup" data-popup-part="list-surface" className={cn(popupClasses, className)} {...props} > {children} </ContextMenuPrimitive.Popup> </ContextMenuPrimitive.Positioner> </ContextMenuPrimitive.Portal> );}export function ContextMenuItem({ className, inset = false, ...props }: ContextMenuItemProps) { return ( <ContextMenuPrimitive.Item data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="item" data-control-family="popup" data-popup-part="item" data-inset={inset || undefined} className={cn(itemClasses, className)} {...props} /> );}export function ContextMenuCheckboxItem({ className, children, ...props }: ContextMenuCheckboxItemProps) { return ( <ContextMenuPrimitive.CheckboxItem data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="checkbox-item" data-control-family="popup" data-popup-part="item" className={cn(itemClasses, className)} {...props} > <span className="pointer-events-none absolute start-2 flex size-4 items-center justify-center"> <ContextMenuPrimitive.CheckboxItemIndicator> <svg viewBox="0 0 12 12" className="size-3" aria-hidden="true" fill="none"> <path d="M2.5 6.5 5 9l4.5-5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /> </svg> </ContextMenuPrimitive.CheckboxItemIndicator> </span> {children} </ContextMenuPrimitive.CheckboxItem> );}export function ContextMenuRadioGroup<TValue extends string = string>({ className, ...props }: ContextMenuRadioGroupProps<TValue>) { return ( <ContextMenuPrimitive.RadioGroup data-control-ui="context-menu" data-control-family="popup" data-popup-kind="context-menu" data-slot="radio-group" className={className} {...props} /> );}export function ContextMenuRadioItem({ className, children, ...props }: ContextMenuRadioItemProps) { return ( <ContextMenuPrimitive.RadioItem data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="radio-item" data-control-family="popup" data-popup-part="item" className={cn(itemClasses, className)} {...props} > <span className="pointer-events-none absolute start-2 flex size-4 items-center justify-center"> <ContextMenuPrimitive.RadioItemIndicator> <span data-control-ui="context-menu" data-control-family="popup" data-popup-kind="context-menu" data-slot="radio-indicator" className="block size-1.5" /> </ContextMenuPrimitive.RadioItemIndicator> </span> {children} </ContextMenuPrimitive.RadioItem> );}export function ContextMenuLabel({ className, inset = false, ...props }: ContextMenuLabelProps) { return ( <ContextMenuPrimitive.GroupLabel data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="label" data-control-family="popup" data-popup-part="label" data-inset={inset || undefined} className={cn(className)} {...props} /> );}export function ContextMenuSeparator({ className, ...props }: ContextMenuSeparatorProps) { return ( <ContextMenuPrimitive.Separator data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="separator" data-control-family="popup" data-popup-part="separator" className={cn(className)} {...props} /> );}export function ContextMenuShortcut({ className, ...props }: ContextMenuShortcutProps) { return ( <span data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="shortcut" data-control-family="popup" data-popup-part="shortcut" className={cn(className)} {...props} /> );}export function ContextMenuSub(props: ContextMenuSubProps) { return <ContextMenuPrimitive.SubmenuRoot {...props} />;}export function ContextMenuSubTrigger({ className, inset = false, children, ...props }: ContextMenuSubTriggerProps) { return ( <ContextMenuPrimitive.SubmenuTrigger data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="sub-trigger" data-control-family="popup" data-popup-part="item" data-inset={inset || undefined} className={cn(itemClasses, className)} {...props} > {children} <span data-control-ui="context-menu" data-popup-kind="context-menu" data-control-family="popup" data-slot="sub-trigger-indicator" aria-hidden="true" > › </span> </ContextMenuPrimitive.SubmenuTrigger> );}export function ContextMenuSubContent({ className, children, ...props }: ContextMenuSubContentProps) { const skin = useSkin(); return ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Positioner data-skin={skin.id} data-effects={controlEffectsAttribute(skin.effects)} className="z-(--z-popup)" sideOffset={-4} alignOffset={-5} > <ContextMenuPrimitive.Popup data-control-ui="context-menu" data-popup-kind="context-menu" data-slot="sub-content" data-surface="floating" data-control-family="popup" data-popup-part="list-surface" className={cn(popupClasses, className)} {...props} > {children} </ContextMenuPrimitive.Popup> </ContextMenuPrimitive.Positioner> </ContextMenuPrimitive.Portal> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-popup-* · 16 knobsHow the cascade resolves--cui-popup-radius*var(--radius-popover)--cui-popup-background<color>var(--popover)--cui-popup-foreground<color>var(--popover-foreground)--cui-popup-border-color<color>var(--control-rim)--cui-popup-border-width<length>var(--control-rim-width)--cui-popup-shadow*var(--shadow-pop)--cui-popup-backdrop-filter*blur(var(--backdrop-blur-popover))--cui-popup-backdrop-background<color>oklch(from var(--foreground) l c h / var(--overlay-opacity))--cui-popup-item-radius<length-percentage>var(--radius-popup-item-fit)--cui-popup-item-foreground<color>var(--foreground)--cui-popup-item-highlight-background<color>var(--hover-fill)--cui-popup-item-highlight-foreground<color>var(--cui-popup-item-foreground)--cui-popup-item-highlight-muted-foreground<color>var(--muted-foreground)--cui-popup-item-disabled-opacity<number>0.4--cui-popup-separator-color<color>var(--border)--cui-popup-shortcut-foreground<color>var(--muted-foreground)