Components
Context Menu
The brand's menu opened with a right click or a long press on an item.
npx -y github:anwarlemu/opencommerce-brand-cli#release add context-menu --surface siteRun from the target React 19.2+ app root with Tailwind CSS 4 and the @/* source alias. Source and examples as JSON.
Usage
Use for shortcuts on an item in a list or table (copy an order ID, retry a checkout). Every action in it must also be reachable another way, such as a row's menu key. Use ContextMenuShortcut for keys, ContextMenuSub for one level of submenu and variant="destructive" on a destructive item.
- The only way to reach an action
- More than one level of submenu
- A context menu on text people want to select
Exports: ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger. This component includes a client boundary. Keep event handlers and state in a client component.
Dependencies: surface, @radix-ui/react-context-menu, lucide-react.
Example
"use client";
import * as React from "react";
import { Copy, ExternalLink, RotateCcw, Trash2 } from "lucide-react";
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
function Order({ defaultOpen = false }: { defaultOpen?: boolean }) {
const [status, setStatus] = React.useState("Right click the order, or press and hold on a phone.");
const [notify, setNotify] = React.useState(true);
const [speed, setSpeed] = React.useState("standard");
return (
<div className="grid w-full max-w-110 gap-3">
<ContextMenu modal={!defaultOpen}>
<ContextMenuTrigger className="grid gap-1 rounded-panel border border-line-window bg-surface p-5 shadow-card">
<span className="flex items-baseline justify-between gap-3">
<span className="font-display text-h5 font-medium tracking-tight text-fg">Northwind Outfitters</span>
<span className="font-mono text-snippet text-fg tabular-nums">$222.00</span>
</span>
<span className="font-mono text-bar text-fg-muted">ord_8KQ2 · placed 2 minutes ago</span>
</ContextMenuTrigger>
<ContextMenuContent className="w-60">
<ContextMenuLabel>Order ord_8KQ2</ContextMenuLabel>
<ContextMenuItem onSelect={() => setStatus("Copied ord_8KQ2.")}>
<Copy strokeWidth={1.5} />
Copy order ID
<ContextMenuShortcut>⌘C</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem onSelect={() => setStatus("Opened the merchant's receipt.")}>
<ExternalLink strokeWidth={1.5} />
Open receipt
</ContextMenuItem>
<ContextMenuItem onSelect={() => setStatus("Checkout retried.")}>
<RotateCcw strokeWidth={1.5} />
Retry checkout
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>Shipping</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuRadioGroup value={speed} onValueChange={setSpeed}>
<ContextMenuRadioItem value="standard">Standard</ContextMenuRadioItem>
<ContextMenuRadioItem value="express">Express</ContextMenuRadioItem>
<ContextMenuRadioItem value="overnight">Overnight</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuCheckboxItem checked={notify} onCheckedChange={(on) => setNotify(on === true)}>
Email me on delivery
</ContextMenuCheckboxItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive" onSelect={() => setStatus("Order canceled.")}>
<Trash2 strokeWidth={1.5} />
Cancel order
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<p role="status" className="text-body-sm text-fg-muted">
{status} Shipping: {speed}. Delivery email {notify ? "on" : "off"}.
</p>
</div>
);
}
export function Site() {
return <Order />;
}Source
"use client";
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight } from "lucide-react";
import { usePortalContainer } from "@/lib/surface";
import { cn } from "@/lib/utils";
// The brand's menu (dropdown-menu.tsx) opened with a right click or a long press on a surface: a floating white panel (a 9
// percent ink edge, 8px corners, a soft drop, 4px of padding), rows in 14px Inter with 5px corners that take a faint
// wash when highlighted, with no transition (menus are worked from the keyboard). It grows from the pointer (scale 0.97
// and a fade, 160ms ease-out); reduced motion keeps the fade. Checks are drawn in blue; destructive rows are red.
const PANEL =
"z-50 flex max-h-(--radix-context-menu-content-available-height) max-w-(--radix-context-menu-content-available-width) origin-(--radix-context-menu-content-transform-origin) animate-pop-in flex-col gap-0.5 overflow-y-auto overscroll-contain rounded-control border border-line bg-surface p-1 text-fg shadow-floating outline-none motion-reduce:animate-none";
const ROW =
"relative flex w-full cursor-default items-center gap-2 rounded-menu px-2.5 py-2 font-sans text-body-sm leading-snug text-fg-muted outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-40 data-[highlighted]:bg-wash-hover data-[highlighted]:text-fg data-[inset]:pl-8 [&_svg]:size-3.5 [&_svg]:shrink-0";
const DANGER = "text-danger data-[highlighted]:bg-danger/6 data-[highlighted]:text-danger";
const QUIET = "text-fg-faint";
function ContextMenu(props: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...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 ContextMenuGroup(props: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />;
}
function ContextMenuPortal(props: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" container={usePortalContainer()} {...props} />;
}
function ContextMenuSub(props: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup(props: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;
}
function ContextMenuContent({ className, collisionPadding = 8, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal container={usePortalContainer()}>
<ContextMenuPrimitive.Content data-slot="context-menu-content" collisionPadding={collisionPadding} className={cn(PANEL, "min-w-[min(12rem,var(--radix-context-menu-content-available-width))]", className)} {...props} />
</ContextMenuPrimitive.Portal>
);
}
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(ROW, variant === "destructive" && DANGER, className)} {...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(ROW, "data-[state=open]:bg-wash-hover data-[state=open]:text-fg", className)} {...props}>
{children}
<ChevronRight className="ml-auto" strokeWidth={1.5} />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({ className, collisionPadding = 8, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.Portal container={usePortalContainer()}>
<ContextMenuPrimitive.SubContent data-slot="context-menu-sub-content" collisionPadding={collisionPadding} className={cn(PANEL, "min-w-[min(10rem,var(--radix-context-menu-content-available-width))]", className)} {...props} />
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem data-slot="context-menu-checkbox-item" checked={checked} className={cn(ROW, "pr-8", className)} {...props}>
{children}
<ContextMenuPrimitive.ItemIndicator className="absolute right-2.5 text-fg-brand">
<Check strokeWidth={1.75} />
</ContextMenuPrimitive.ItemIndicator>
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem data-slot="context-menu-radio-item" className={cn(ROW, "pr-8 data-[state=checked]:text-fg", className)} {...props}>
{children}
<ContextMenuPrimitive.ItemIndicator className="absolute right-2.5 text-fg-brand">
<Check strokeWidth={1.75} />
</ContextMenuPrimitive.ItemIndicator>
</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-caption data-[inset]:pl-8", QUIET, 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", 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-caption", QUIET, className)} {...props} />;
}
export {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuPortal,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
};Reference: Extension from opencommerce-agentcard-web src/app/globals.css (.nav-tray) and dropdown-menu.tsx.