Context Menu

import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup } from '@hanzo/ui'

Default

Right-click or long-press the area to open a menu anchored at the cursor.

Right-click here
export function Default() {
return (
<ContextMenu>
<ContextMenuTrigger height={120} items="center" justify="center" rounded="$4" borderWidth={1} borderStyle="dashed" borderColor="$borderColor" >
<Text color="$quiet">Right-click here</Text>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>
Back
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Forward
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Reload
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>Save page as…</ContextMenuItem>
<ContextMenuItem>Print…</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}

Variants

A destructive row for an action that removes something, a disabled row that cannot be chosen, and inset rows that line up with rows carrying an icon.

quarterly-report.pdf
export function Variants() {
return (
<ContextMenu>
<ContextMenuTrigger height={120} items="center" justify="center" rounded="$4" borderWidth={1} borderStyle="dashed" borderColor="$borderColor" >
<Text color="$quiet">quarterly-report.pdf</Text>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem variant="default">
<Pencil size={16} />
Rename
</ContextMenuItem>
<ContextMenuItem>
<Copy size={16} />
Duplicate
</ContextMenuItem>
<ContextMenuItem inset>Move to…</ContextMenuItem>
<ContextMenuItem inset disabled>
Paste
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">
<Trash2 size={16} />
Delete
<ContextMenuShortcut>⌘⌫</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}

Controlled

Checkbox rows toggle view options and a radio group picks one sort order, both held in React state so the trigger can show them.

Word wrap on · minimap off · sorted by name
export function Controlled() {
const [wrap, setWrap] = useState(true)
const [minimap, setMinimap] = useState(false)
const [sort, setSort] = useState("name")
return (
<ContextMenu>
<ContextMenuTrigger height={120} items="center" justify="center" rounded="$4" borderWidth={1} borderStyle="dashed" borderColor="$borderColor" >
<Text color="$quiet">
Word wrap {wrap ? "on" : "off"} · minimap {minimap ? "on" : "off"} ·
sorted by {sort}
</Text>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuLabel inset>View</ContextMenuLabel>
<ContextMenuCheckboxItem checked={wrap} onCheckedChange={setWrap}>
Word wrap
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem checked={minimap} onCheckedChange={setMinimap} >
Minimap
</ContextMenuCheckboxItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuLabel inset>Sort by</ContextMenuLabel>
<ContextMenuRadioGroup value={sort} onValueChange={setSort}>
<ContextMenuRadioItem value="name">Name</ContextMenuRadioItem>
<ContextMenuRadioItem value="modified">
Date modified
</ContextMenuRadioItem>
<ContextMenuRadioItem value="size">Size</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
)
}

Nested

A sub trigger opens a second panel beside the first; wrap it in `ContextMenuPortal` so it escapes the root panel, which portals itself.

launch-notes.md
export function Nested() {
return (
<ContextMenu>
<ContextMenuTrigger height={120} items="center" justify="center" rounded="$4" borderWidth={1} borderStyle="dashed" borderColor="$borderColor" >
<Text color="$quiet">launch-notes.md</Text>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem>Rename</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger key="share">Share</ContextMenuSubTrigger>
<ContextMenuPortal>
<ContextMenuSubContent>
<ContextMenuItem>
<Link size={16} />
Copy link
</ContextMenuItem>
<ContextMenuItem>
<Mail size={16} />
Email
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>Publish to web</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuPortal>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Move to trash</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}

Open state

`onOpenChange` reports each open and close, so the target can stay highlighted while its menu is up.

Right-click here
export function OpenState() {
const [open, setOpen] = useState(false)
return (
<ContextMenu onOpenChange={setOpen}>
<ContextMenuTrigger height={120} items="center" justify="center" rounded="$4" borderWidth={1} borderStyle={open ? "solid" : "dashed"} borderColor={open ? "$ink" : "$borderColor"} bg={open ? "$raised" : undefined} >
<Text color={open ? "$ink" : "$quiet"}>
{open ? "Menu open" : "Right-click here"}
</Text>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Cut</ContextMenuItem>
<ContextMenuItem>Copy</ContextMenuItem>
<ContextMenuItem>Paste</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}

Types

export type ContextMenuContentProps = React.ComponentProps<typeof GuiContextMenu.Content>
export type ContextMenuSubContentProps = React.ComponentProps<typeof GuiContextMenu.SubContent>
export type ContextMenuItemProps = Omit<
React.ComponentProps<typeof GuiContextMenu.Item>,
'inset' | 'variant'
> & {
inset?: boolean
variant?: 'default' | 'destructive'
}

Exports

ContextMenuContextMenuTriggerContextMenuContentContextMenuItemContextMenuCheckboxItemContextMenuRadioItemContextMenuLabelContextMenuSeparatorContextMenuShortcutContextMenuGroupContextMenuPortalContextMenuSubContextMenuSubContentContextMenuSubTriggerContextMenuRadioGrouptype ContextMenuContentPropstype ContextMenuSubContentPropstype ContextMenuItemProps