A right-click menu styled like macOS: frosted panel, accent-color highlight, key equivalents, checkmarks, submenus and a red destructive item, on Radix for full keyboard support.
import { Copy, ExternalLink, Info, Share, Tag, Trash2 } from "lucide-react"
import { FileIcon } from "@/components/ballmac/mac-icons"
import {
MacContextMenu,
MacContextMenuContent,
MacContextMenuItem,
MacContextMenuLabel,
MacContextMenuSeparator,
MacContextMenuSub,
MacContextMenuSubContent,
MacContextMenuSubTrigger,
MacContextMenuTrigger,
} from "@/components/ballmac/mac-context-menu"
const tags = ["Red", "Orange", "Green", "Blue"]
export default function MacContextMenuDemo() {
return (
<MacContextMenu>
<MacContextMenuTrigger className="flex h-56 w-full max-w-xl select-none flex-col items-center justify-center gap-3 rounded-2xl border border-dashed bg-card text-sm text-muted-foreground">
<FileIcon size={64} label="pdf" tone="red" />
<span className="font-medium text-foreground">Q3 Report.pdf</span>
<span>Right-click, or long-press on touch</span>
</MacContextMenuTrigger>
<MacContextMenuContent className="w-64">
<MacContextMenuItem icon={<ExternalLink />} shortcut="⌘O">Open</MacContextMenuItem>
<MacContextMenuItem icon={<Info />} shortcut="⌘I">Get Info</MacContextMenuItem>
<MacContextMenuSeparator />
<MacContextMenuItem icon={<Copy />} shortcut="⌘D">Duplicate</MacContextMenuItem>
<MacContextMenuSub>
<MacContextMenuSubTrigger icon={<Tag />}>Tags</MacContextMenuSubTrigger>
<MacContextMenuSubContent>
{tags.map((t) => (
<MacContextMenuItem key={t}>{t}</MacContextMenuItem>
))}
</MacContextMenuSubContent>
</MacContextMenuSub>
<MacContextMenuItem icon={<Share />}>Share…</MacContextMenuItem>
<MacContextMenuSeparator />
<MacContextMenuLabel>Danger</MacContextMenuLabel>
<MacContextMenuItem destructive icon={<Trash2 />} shortcut="⌘⌫">Move to Bin</MacContextMenuItem>
</MacContextMenuContent>
</MacContextMenu>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/mac-context-menuInstall the dependencies.
$ pnpm add radix-ui lucide-reactCopy the source into your project.
components/ballmac/mac-context-menu.tsx// Ballmac UI: Mac Context Menu. https://ui.ballmac.com/components/mac-context-menu "use client" import * as React from "react" import { Check, ChevronRight } from "lucide-react" import { ContextMenu as Primitive } from "radix-ui" import { cn } from "@/lib/utils" const content = "z-50 min-w-[13.5rem] overflow-hidden rounded-[10px] border border-foreground/10 bg-popover/80 p-1.5 text-[13px] text-popover-foreground shadow-[0_12px_40px_-8px_rgb(0_0_0/0.35),0_2px_8px_rgb(0_0_0/0.12)] backdrop-blur-2xl backdrop-saturate-150 outline-none dark:bg-popover/70 [--mac-accent:oklch(0.53_0.2_258)] data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 motion-reduce:animate-none" const item = "relative flex h-[22px] cursor-default items-center gap-2 rounded-[5px] pe-2.5 ps-[22px] outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:text-foreground/35 data-[highlighted]:bg-(--mac-accent) data-[highlighted]:text-white [&_svg]:size-3.5 [&_svg]:shrink-0" function MacContextMenu(props: React.ComponentProps<typeof Primitive.Root>) { return <Primitive.Root {...props} /> } function MacContextMenuTrigger({ className, ...props }: React.ComponentProps<typeof Primitive.Trigger>) { return <Primitive.Trigger data-slot="mac-context-menu-trigger" className={className} {...props} /> } function MacContextMenuContent({ className, ...props }: React.ComponentProps<typeof Primitive.Content>) { return ( <Primitive.Portal> <Primitive.Content data-slot="mac-context-menu-content" collisionPadding={8} className={cn(content, className)} {...props} /> </Primitive.Portal> ) } type MacContextMenuItemProps = React.ComponentProps<typeof Primitive.Item> & { /** Leading icon, drawn in the left gutter. */ icon?: React.ReactNode /** Key equivalent shown at the right, such as ⌘C. */ shortcut?: string /** Destructive items turn red until highlighted. */ destructive?: boolean } function MacContextMenuItem({ icon, shortcut, destructive, className, children, ...props }: MacContextMenuItemProps) { return ( <Primitive.Item data-slot="mac-context-menu-item" data-destructive={destructive ? "" : undefined} className={cn(item, icon && "ps-[30px]", destructive && "text-destructive data-[highlighted]:text-white", className)} {...props}> {icon && <span aria-hidden="true" className="absolute start-2 flex items-center">{icon}</span>} <span className="flex-1 truncate">{children}</span> {shortcut && <MacContextMenuShortcut>{shortcut}</MacContextMenuShortcut>} </Primitive.Item> ) } function MacContextMenuCheckboxItem({ className, children, shortcut, ...props }: React.ComponentProps<typeof Primitive.CheckboxItem> & { shortcut?: string }) { return ( <Primitive.CheckboxItem data-slot="mac-context-menu-checkbox-item" className={cn(item, className)} {...props}> <Primitive.ItemIndicator className="absolute start-1.5 flex items-center"> <Check aria-hidden="true" strokeWidth={3} /> </Primitive.ItemIndicator> <span className="flex-1 truncate">{children}</span> {shortcut && <MacContextMenuShortcut>{shortcut}</MacContextMenuShortcut>} </Primitive.CheckboxItem> ) } function MacContextMenuRadioGroup(props: React.ComponentProps<typeof Primitive.RadioGroup>) { return <Primitive.RadioGroup {...props} /> } function MacContextMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof Primitive.RadioItem>) { return ( <Primitive.RadioItem data-slot="mac-context-menu-radio-item" className={cn(item, className)} {...props}> <Primitive.ItemIndicator className="absolute start-1.5 flex items-center"> <Check aria-hidden="true" strokeWidth={3} /> </Primitive.ItemIndicator> <span className="flex-1 truncate">{children}</span> </Primitive.RadioItem> ) } function MacContextMenuLabel({ className, ...props }: React.ComponentProps<typeof Primitive.Label>) { return <Primitive.Label data-slot="mac-context-menu-label" className={cn("px-[22px] pt-1 pb-0.5 text-[11px] font-semibold text-muted-foreground", className)} {...props} /> } function MacContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof Primitive.Separator>) { return <Primitive.Separator data-slot="mac-context-menu-separator" className={cn("mx-2.5 my-1 h-px bg-foreground/12", className)} {...props} /> } function MacContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { return <span data-slot="mac-context-menu-shortcut" aria-hidden="true" className={cn("ms-6 text-[12px] tracking-wide text-muted-foreground group-data-[highlighted]:text-white/80 [[data-highlighted]_&]:text-white/80", className)} {...props} /> } function MacContextMenuSub(props: React.ComponentProps<typeof Primitive.Sub>) { return <Primitive.Sub {...props} /> } function MacContextMenuSubTrigger({ className, children, icon, ...props }: React.ComponentProps<typeof Primitive.SubTrigger> & { icon?: React.ReactNode }) { return ( <Primitive.SubTrigger data-slot="mac-context-menu-sub-trigger" className={cn(item, icon && "ps-[30px]", "data-[state=open]:bg-foreground/10", className)} {...props}> {icon && <span aria-hidden="true" className="absolute start-2 flex items-center">{icon}</span>} <span className="flex-1 truncate">{children}</span> <ChevronRight aria-hidden="true" className="ms-4 -me-1 rtl:rotate-180" strokeWidth={2.5} /> </Primitive.SubTrigger> ) } function MacContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof Primitive.SubContent>) { return ( <Primitive.Portal> <Primitive.SubContent data-slot="mac-context-menu-sub-content" collisionPadding={8} className={cn(content, "shadow-lg", className)} {...props} /> </Primitive.Portal> ) } export { MacContextMenu, MacContextMenuTrigger, MacContextMenuContent, MacContextMenuItem, MacContextMenuCheckboxItem, MacContextMenuRadioGroup, MacContextMenuRadioItem, MacContextMenuLabel, MacContextMenuSeparator, MacContextMenuShortcut, MacContextMenuSub, MacContextMenuSubTrigger, MacContextMenuSubContent, type MacContextMenuItemProps, }Update the import paths to match your project setup.
Usage
import { MacContextMenu, MacContextMenuTrigger, MacContextMenuContent, MacContextMenuItem, MacContextMenuCheckboxItem, MacContextMenuRadioGroup, MacContextMenuRadioItem, MacContextMenuLabel, MacContextMenuSeparator, MacContextMenuShortcut, MacContextMenuSub, MacContextMenuSubTrigger, MacContextMenuSubContent } from "@/components/ballmac/mac-context-menu"The full example is in the Code tab above.
Examples
Checkmarks and radio items
"use client"
import * as React from "react"
import {
MacContextMenu,
MacContextMenuCheckboxItem,
MacContextMenuContent,
MacContextMenuLabel,
MacContextMenuRadioGroup,
MacContextMenuRadioItem,
MacContextMenuSeparator,
MacContextMenuTrigger,
} from "@/components/ballmac/mac-context-menu"
export default function MacContextMenuView() {
const [sort, setSort] = React.useState("name")
const [hidden, setHidden] = React.useState(false)
const [preview, setPreview] = React.useState(true)
return (
<MacContextMenu>
<MacContextMenuTrigger className="flex h-48 w-full max-w-xl select-none items-center justify-center rounded-2xl border border-dashed bg-card px-4 text-center text-sm text-muted-foreground">
<span>
Sorted by <strong className="text-foreground capitalize">{sort}</strong>
{hidden ? ", hidden files shown" : ""}
{preview ? ", preview on" : ""}. Right-click to change.
</span>
</MacContextMenuTrigger>
<MacContextMenuContent className="w-60">
<MacContextMenuLabel>Sort By</MacContextMenuLabel>
<MacContextMenuRadioGroup value={sort} onValueChange={setSort}>
<MacContextMenuRadioItem value="name">Name</MacContextMenuRadioItem>
<MacContextMenuRadioItem value="kind">Kind</MacContextMenuRadioItem>
<MacContextMenuRadioItem value="date">Date Modified</MacContextMenuRadioItem>
</MacContextMenuRadioGroup>
<MacContextMenuSeparator />
<MacContextMenuCheckboxItem checked={hidden} onCheckedChange={setHidden}>Show Hidden Files</MacContextMenuCheckboxItem>
<MacContextMenuCheckboxItem checked={preview} onCheckedChange={setPreview}>Show Preview</MacContextMenuCheckboxItem>
</MacContextMenuContent>
</MacContextMenu>
)
}API reference
| Prop | Type | Default |
|---|---|---|
iconLeading icon, drawn in the left gutter. | React.ReactNode | — |
shortcutKey equivalent shown at the right, such as ⌘C. | string | — |
destructiveDestructive items turn red until highlighted. | boolean | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Shift+F10 / ContextMenu key | Opens the menu on the focused trigger |
| ArrowUp / ArrowDown | Moves through items; ArrowRight opens a submenu |
| Escape | Closes |
Use with AI
Compose <MacContextMenu><MacContextMenuTrigger/><MacContextMenuContent> with MacContextMenuItem (icon, shortcut, destructive), CheckboxItem, RadioGroup/RadioItem, Label, Separator and Sub parts. Shift+F10 and long-press also open it. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Mac Context Menu (@ballmac/mac-context-menu) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Desktop-like apps and file managers
- Anywhere you want the macOS look
Not for
- Standard product UI (context-menu)
- Click-opened menus (dropdown-menu)
Registry JSON: https://ui.ballmac.com/r/mac-context-menu.json
Credits
Free to use in personal and commercial projects.
- Registry
- shadcn/utils
Pairs well with
Finder Window
A working mini Finder: sidebar shortcuts, back and forward history, icon and list views, search, click, shift and command selection, keyboard navigation and a path bar, on a macOS window.
Desktop Icons
A macOS desktop of icons: box-select with the pointer, drag to move with snapping to grid cells, double-click or Return to open, arrow keys to move focus and Alt plus arrows to move an icon.
Context Menu
A right-click, long-press and Shift+F10 menu with submenus, checkbox and radio items, shortcut hints and a destructive style, built on Radix.
Control Center
The macOS Control Center panel in frosted glass: toggle tiles with round badges, grouped Wi-Fi, Bluetooth and AirDrop rows, pill sliders with the icon in the track, and a Now Playing tile. Real toggle buttons and Radix sliders.