Overlays
Context Menu.
A menu opened by right-clicking an area, with labels, shortcuts, checkbox and radio items and submenus.
Right-click hereLong-press on touch screens
"use client";
import * as React from "react";
import { CopyIcon, FolderInputIcon, MousePointerClickIcon, PencilIcon, RefreshCwIcon, Trash2Icon } from "lucide-react";
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export default function ContextMenuDemo() {
const [showHidden, setShowHidden] = React.useState(false);
const [showSizes, setShowSizes] = React.useState(true);
const [sort, setSort] = React.useState("name");
return (
<ContextMenu>
<ContextMenuTrigger tabIndex={0} className="flex h-44 w-full max-w-sm flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-input bg-accent bg-grid text-muted-foreground transition-colors outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring/25 data-[state=open]:border-primary data-[state=open]:text-foreground">
<MousePointerClickIcon className="size-6 text-[#b7ca9e]" strokeWidth={1.5} />
<span className="font-mono text-[10px] tracking-[0.2em] uppercase">Right-click here</span>
<span className="text-xs text-muted-foreground/70">Long-press on touch screens</span>
</ContextMenuTrigger>
<ContextMenuContent className="w-60">
<ContextMenuLabel>/disk/programs</ContextMenuLabel>
<ContextMenuItem>
<PencilIcon />
Rename
<ContextMenuShortcut>F2</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
<CopyIcon />
Copy path
<ContextMenuShortcut>Ctrl C</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>
<FolderInputIcon />
Move to
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-44">
<ContextMenuItem>/disk/archive</ContextMenuItem>
<ContextMenuItem>/disk2/backup</ContextMenuItem>
<ContextMenuItem>/rom/programs</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem>
<RefreshCwIcon />
Refresh
<ContextMenuShortcut>Ctrl R</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuCheckboxItem checked={showHidden} onCheckedChange={setShowHidden}>
Show hidden files
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem checked={showSizes} onCheckedChange={setShowSizes}>
Show file sizes
</ContextMenuCheckboxItem>
<ContextMenuSeparator />
<ContextMenuLabel inset>Sort by</ContextMenuLabel>
<ContextMenuRadioGroup value={sort} onValueChange={setSort}>
<ContextMenuRadioItem value="name">Name</ContextMenuRadioItem>
<ContextMenuRadioItem value="size">Size</ContextMenuRadioItem>
<ContextMenuRadioItem value="modified">Last modified</ContextMenuRadioItem>
</ContextMenuRadioGroup>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">
<Trash2Icon />
Delete
<ContextMenuShortcut>Del</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}Installation#
With the CLI
$npx lanterncn add context-menuThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/context-menu. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install radix-ui lucide-reactCopy the source into your project
components/ui/context-menu.tsx"use client"; import * as React from "react"; import { ContextMenu as ContextMenuPrimitive } from "radix-ui"; import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; import { cn } from "@/lib/utils"; const menuPanel = "z-50 min-w-[10rem] overflow-x-hidden overflow-y-auto rounded-lg border bg-popover p-1 text-popover-foreground shadow-block-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 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"; const menuItem = "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-[13px] outline-none select-none focus:bg-secondary focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground"; function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />; } function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />; } function ContextMenuTrigger({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { return ( <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" className={cn("select-none [-webkit-touch-callout:none]", className)} {...props} /> ); } function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) { return ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Content data-slot="context-menu-content" className={cn( menuPanel, "max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin)", className, )} {...props} /> </ContextMenuPrimitive.Portal> ); } function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />; } function ContextMenuItem({ className, inset, variant = "default", ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { inset?: boolean; variant?: "default" | "destructive"; }) { return ( <ContextMenuPrimitive.Item data-slot="context-menu-item" data-inset={inset} data-variant={variant} className={cn( menuItem, "data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/12 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:[&_svg]:!text-destructive", className, )} {...props} /> ); } function ContextMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) { return ( <ContextMenuPrimitive.CheckboxItem data-slot="context-menu-checkbox-item" className={cn(menuItem, "pl-8", className)} checked={checked} {...props} > <span className="pointer-events-none absolute left-2 flex size-4 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <CheckIcon className="size-4 text-primary" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.CheckboxItem> ); } function ContextMenuRadioGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) { return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />; } function ContextMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) { return ( <ContextMenuPrimitive.RadioItem data-slot="context-menu-radio-item" className={cn(menuItem, "pl-8", className)} {...props} > <span className="pointer-events-none absolute left-2 flex size-4 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <CircleIcon className="size-2 fill-primary text-primary" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.RadioItem> ); } function ContextMenuLabel({ className, inset, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { inset?: boolean }) { return ( <ContextMenuPrimitive.Label data-slot="context-menu-label" data-inset={inset} className={cn( "px-2 pt-2 pb-1.5 font-mono text-[10px] font-semibold tracking-[0.2em] text-success uppercase data-[inset]:pl-8", className, )} {...props} /> ); } function ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) { 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 pl-4 font-mono text-[10px] tracking-[0.12em] text-muted-foreground uppercase", className)} {...props} /> ); } function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />; } function ContextMenuSubTrigger({ className, inset, children, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { inset?: boolean }) { return ( <ContextMenuPrimitive.SubTrigger data-slot="context-menu-sub-trigger" data-inset={inset} className={cn(menuItem, "data-[inset]:pl-8 data-[state=open]:bg-secondary", className)} {...props} > {children} <ChevronRightIcon className="ml-auto size-4" /> </ContextMenuPrimitive.SubTrigger> ); } function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { return ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.SubContent data-slot="context-menu-sub-content" className={cn(menuPanel, "origin-(--radix-context-menu-content-transform-origin)", className)} {...props} /> </ContextMenuPrimitive.Portal> ); } export { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuPortal,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";<ContextMenu>
<ContextMenuTrigger>Right-click here</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Rename</ContextMenuItem>
<ContextMenuItem>Copy path</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>On touch devices the menu opens on a long-press instead of a right-click. From the keyboard, focus the trigger and press Shift F10 or the context menu key.
The API matches Dropdown Menu: ContextMenuItem takes inset and variant="destructive", and there are checkbox, radio and sub items.