Component
Context Menu
The dropdown menu's panel and rows, opened with a right click or a long press on an area.
npx -y github:anwarlemu/inkbox-brand-cli#release add context-menuUsage
Use for shortcuts to actions on an item in a list (an email in the inbox, a contact). Every action must also be reachable another way, such as a row's menu button.
Avoid
- Actions that exist only in the context menu
- A context menu on text people need to select
- Nested submenus beyond one level
Exports: ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger. This component is a client component.
Dependencies: surface, radix-ui, @phosphor-icons/react.
Example
"use client";import * as React from "react";import { ArchiveIcon, ArrowBendUpLeftIcon, ArrowBendUpRightIcon, EnvelopeOpenIcon, ProhibitIcon, TrashIcon, WarningIcon } from "@phosphor-icons/react/ssr";import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger,} from "@/components/ui/context-menu";export function Preview() { const [starred, setStarred] = React.useState(false); const [folder, setFolder] = React.useState("inbox"); return ( <ContextMenu> <ContextMenuTrigger className="mx-auto grid w-full max-w-96 gap-1 rounded-control bg-subtle p-3"> <span className="flex justify-between text-body text-fg"> Ray Liao <span className="text-fg-muted">Yesterday</span> </span> <span className="text-body text-fg">Inkbox A2A: publish your Agent Card</span> <span className="truncate text-body text-fg-brand">Hello @acme, This is Ray, cofounder of Inkbox.</span> <span className="mt-2 text-caption text-fg-muted">Right-click or long-press the email</span> </ContextMenuTrigger> <ContextMenuContent className="w-60"> <ContextMenuItem> <ArrowBendUpLeftIcon /> Reply <ContextMenuShortcut>R</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem> <ArrowBendUpRightIcon /> Forward <ContextMenuShortcut>F</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem> <EnvelopeOpenIcon weight="fill" /> Mark as unread </ContextMenuItem> <ContextMenuCheckboxItem checked={starred} onCheckedChange={(v) => setStarred(v === true)}> Starred </ContextMenuCheckboxItem> <ContextMenuSub> <ContextMenuSubTrigger> <ArchiveIcon weight="fill" /> Move to </ContextMenuSubTrigger> <ContextMenuSubContent className="w-44"> <ContextMenuLabel>Folders</ContextMenuLabel> <ContextMenuRadioGroup value={folder} onValueChange={setFolder}> <ContextMenuRadioItem value="inbox">Inbox</ContextMenuRadioItem> <ContextMenuRadioItem value="sent">Sent</ContextMenuRadioItem> <ContextMenuRadioItem value="spam">Spam</ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuSeparator /> <ContextMenuItem> <WarningIcon weight="fill" /> Report spam </ContextMenuItem> <ContextMenuItem> <ProhibitIcon /> Block sender </ContextMenuItem> <ContextMenuItem variant="destructive"> <TrashIcon weight="fill" /> Delete <ContextMenuShortcut>⌫</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> );}Source
"use client";import * as React from "react";import { ContextMenu as ContextMenuPrimitive } from "radix-ui";import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react/ssr";import { usePortalContainer } from "@/lib/surface";import { cn } from "@/lib/utils";// A right-click menu, extrapolated from the console (Figma 427:2715), drawn as the dropdown menu: the panel is a raised white panel (8px corners, the floating// shadow) with 4px of padding; items are 36px rows with 8px corners like the rail's nav rows, a 20px glyph in the// action-glyph purple 10px from a 14px ink label, and the lilac under the highlighted row. Section labels are 13px in// the label purple ("Channels"), separators the rail's hairline, shortcuts 12px muted, destructive items red. The// panel grows out of its trigger from 96 percent with a fade; highlighting a row is instant (menus are used by keyboard// and many times a day).const CONTENT = "z-50 max-h-(--radix-context-menu-content-available-height) min-w-48 overflow-x-hidden overflow-y-auto rounded-control bg-surface p-1 text-fg shadow-floating outline-none data-[state=closed]:animate-pop-out data-[state=open]:animate-pop-in";const ITEM = "relative flex h-9 cursor-default items-center gap-2.5 rounded-control px-2.5 font-sans text-body leading-tight text-fg outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-subtle data-[inset]:pl-10.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5.5 [&_svg:not([class*='text-'])]:text-icon-secondary";function ContextMenu(props: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;}function ContextMenuPortal(props: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { const container = usePortalContainer(); return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" container={container} {...props} />;}function ContextMenuTrigger({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" className={cn("select-none", className)} {...props} />;}function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) { return ( <ContextMenuPortal> <ContextMenuPrimitive.Content data-slot="context-menu-content" className={cn(CONTENT, "origin-(--radix-context-menu-content-transform-origin)", className)} {...props} /> </ContextMenuPortal> );}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 || undefined} data-variant={variant} className={cn(ITEM, "data-[variant=destructive]:text-danger data-[variant=destructive]:[&_svg]:text-danger", className)} {...props} /> );}function ContextMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) { return ( <ContextMenuPrimitive.CheckboxItem data-slot="context-menu-checkbox-item" checked={checked} className={cn(ITEM, "pl-10.5", className)} {...props}> <span className="pointer-events-none absolute left-2.5 flex size-5.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <CheckIcon weight="bold" className="size-4.5 text-fg-brand" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.CheckboxItem> );}function ContextMenuRadioGroup(props: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) { return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;}// The selected choice wears the console's radio mark: a pink ring around white.function ContextMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) { return ( <ContextMenuPrimitive.RadioItem data-slot="context-menu-radio-item" className={cn(ITEM, "pl-10.5", className)} {...props}> <span className="pointer-events-none absolute left-2.5 flex size-5.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <span className="block size-3.5 rounded-pill border-[3.5px] border-pink bg-surface" /> </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 || undefined} className={cn("px-2.5 pt-2 pb-1 font-sans text-body-sm leading-tight text-fg-label data-[inset]:pl-10.5", 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-line-subtle", className)} {...props} />;}function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { return <span data-slot="context-menu-shortcut" className={cn("ml-auto pl-4 font-sans text-caption text-fg-muted", 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 || undefined} className={cn(ITEM, "data-[state=open]:bg-subtle", className)} {...props}> {children} <CaretRightIcon className="ml-auto size-4.5 text-fg-muted" /> </ContextMenuPrimitive.SubTrigger> );}function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { return ( <ContextMenuPortal> <ContextMenuPrimitive.SubContent data-slot="context-menu-sub-content" className={cn(CONTENT, "max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin)", className)} {...props} /> </ContextMenuPortal> );}export { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger,};Reference: Extrapolated from the Inkbox console (Figma 427:2715): the email list items, nav rows and the raised panel.