Context Menu

Right-click context menu with items and submenus. Install Context Menu from the VLLNT UI registry with the shadcn CLI.

Report a bug

Right-click context menu with items and submenus. Part of the overlay family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add Context Menu to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/context-menu.json

Source

"use client"; import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; import { Check, ChevronRight, Circle } from "lucide-react"; import { cn } from "../../lib/utils"; const ContextMenu = ContextMenuPrimitive.Root; const ContextMenuTrigger = ContextMenuPrimitive.Trigger; const ContextMenuGroup = ContextMenuPrimitive.Group; const ContextMenuPortal = ContextMenuPrimitive.Portal; const ContextMenuSub = ContextMenuPrimitive.Sub; const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; const ContextMenuSubTrigger = ({ children, className, inset, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { inset?: boolean; ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.SubTrigger>>; }) => ( <ContextMenuPrimitive.SubTrigger className={cn( "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", inset && "pl-8", className, )} ref={ref} {...props} > {children} <ChevronRight className="ml-auto size-4" /> </ContextMenuPrimitive.SubTrigger> ); ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; const ContextMenuSubContent = ({ className, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> & { ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.SubContent>>; }) => ( <ContextMenuPrimitive.SubContent className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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", className, )} ref={ref} {...props} /> ); ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; const ContextMenuContent = ({ className, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> & { ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.Content>>; }) => ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Content className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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", className, )} ref={ref} {...props} /> </ContextMenuPrimitive.Portal> ); ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; const ContextMenuItem = ({ className, inset, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { inset?: boolean; ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.Item>>; }) => ( <ContextMenuPrimitive.Item className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", className, )} ref={ref} {...props} /> ); ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; const ContextMenuCheckboxItem = ({ checked, children, className, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & { ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.CheckboxItem>>; }) => ( <ContextMenuPrimitive.CheckboxItem checked={checked} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className, )} ref={ref} {...props} > <span className="absolute left-2 flex size-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <Check className="size-4" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.CheckboxItem> ); ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; const ContextMenuRadioItem = ({ children, className, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & { ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.RadioItem>>; }) => ( <ContextMenuPrimitive.RadioItem className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className, )} ref={ref} {...props} > <span className="absolute left-2 flex size-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <Circle className="size-2 fill-current" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.RadioItem> ); ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; const ContextMenuLabel = ({ className, inset, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { inset?: boolean; ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.Label>>; }) => ( <ContextMenuPrimitive.Label className={cn( "px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className, )} ref={ref} {...props} /> ); ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; const ContextMenuSeparator = ({ className, ref, ...props }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> & { ref?: React.Ref<React.ComponentRef<typeof ContextMenuPrimitive.Separator>>; }) => ( <ContextMenuPrimitive.Separator className={cn("-m-1 h-px bg-border", className)} ref={ref} {...props} /> ); ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { return ( <span className={cn( "ml-auto text-xs tracking-widest text-muted-foreground", className, )} {...props} /> ); }; ContextMenuShortcut.displayName = "ContextMenuShortcut"; export { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, };

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0