ContextMenu
NavigationContextual actions with checkable items and nested menus.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubTrigger,
ContextMenuSubContent,
} from "@/components/ui/context-menu";
import { useState } from "react";
export default function ContextMenuExample() {
const [action, setAction] = useState("No action selected");
const [favorite, setFavorite] = useState(false);
return (
<div className="w-full max-w-md space-y-3">
<ContextMenu>
<ContextMenuTrigger asChild>
<div
tabIndex={0}
role="group"
aria-label="Project context actions"
className="flex h-40 items-center justify-center rounded-xl border-2 border-dashed p-6 text-center text-sm text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Right-click, long-press, or use Shift+F10 here
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuLabel>Project actions</ContextMenuLabel>
<ContextMenuItem onSelect={() => setAction("Project opened")}>
Open project
</ContextMenuItem>
<ContextMenuCheckboxItem
checked={favorite}
onCheckedChange={(value) => setFavorite(value === true)}
>
Favorite
</ContextMenuCheckboxItem>
<ContextMenuSub>
<ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onSelect={() => setAction("Moved to Design")}>
Design
</ContextMenuItem>
<ContextMenuItem
onSelect={() => setAction("Moved to Engineering")}
>
Engineering
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem disabled>Transfer ownership</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
destructive
onSelect={() => setAction("Archived in preview")}
>
Archive
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<p role="status" className="text-xs text-muted-foreground">
{action}
{favorite ? " · Favorited" : ""}
</p>
</div>
);
}Usage
Wrap a target in ContextMenuTrigger. Menus support regular actions, disabled items, checkbox items, and nested submenus. Handle onSelect for commands; the demo changes only local preview state.
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubTrigger,
ContextMenuSubContent,
} from "@/components/ui/context-menu";Shipped as editable source in src/components/ui/context-menu.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/context-menu.tsx
"use client";
import { useDocumentDirection } from "@/lib/use-document-direction";
import type { ComponentProps } from "react";
import { cn } from "@dashboardpack/core/lib/utils";
import { ContextMenu as Primitive } from "radix-ui";
import { Check, ChevronRight } from "lucide-react";
export function ContextMenu(props: ComponentProps<typeof Primitive.Root>) {
const dir = useDocumentDirection();
return <Primitive.Root dir={dir} {...props} />;
}
export function ContextMenuTrigger({
onKeyDown,
...props
}: ComponentProps<typeof Primitive.Trigger>) {
return (
<Primitive.Trigger
{...props}
onKeyDown={(event) => {
onKeyDown?.(event);
if (
props.disabled ||
event.defaultPrevented ||
!(
event.key === "ContextMenu" ||
(event.shiftKey && event.key === "F10")
)
)
return;
event.preventDefault();
const rect = event.currentTarget.getBoundingClientRect();
event.currentTarget.dispatchEvent(
new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
button: 2,
clientX: rect.left + Math.min(16, rect.width / 2),
clientY: rect.top + Math.min(16, rect.height / 2),
}),
);
}}
/>
);
}
export const ContextMenuSub = Primitive.Sub;
export function ContextMenuContent({
className,
...props
}: ComponentProps<typeof Primitive.Content>) {
return (
<Primitive.Portal>
<Primitive.Content
className={cn(
"z-50 min-w-48 max-w-[calc(100vw-2rem)] rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg",
className,
)}
{...props}
/>
</Primitive.Portal>
);
}
export function ContextMenuItem({
className,
destructive,
...props
}: ComponentProps<typeof Primitive.Item> & { destructive?: boolean }) {
return (
<Primitive.Item
className={cn(
"flex cursor-default items-center gap-2 rounded-md px-2 py-2 text-sm outline-none focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
destructive && "text-destructive",
className,
)}
{...props}
/>
);
}
export function ContextMenuCheckboxItem({
className,
children,
...props
}: ComponentProps<typeof Primitive.CheckboxItem>) {
return (
<Primitive.CheckboxItem
className={cn(
"relative flex cursor-default items-center gap-2 rounded-md py-2 pe-2 ps-8 text-sm outline-none focus:bg-accent data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute start-2">
<Primitive.ItemIndicator>
<Check className="size-4" />
</Primitive.ItemIndicator>
</span>
{children}
</Primitive.CheckboxItem>
);
}
export function ContextMenuLabel({
className,
...props
}: ComponentProps<typeof Primitive.Label>) {
return (
<Primitive.Label
className={cn(
"px-2 py-2 text-xs font-semibold text-muted-foreground",
className,
)}
{...props}
/>
);
}
export function ContextMenuSeparator(
props: ComponentProps<typeof Primitive.Separator>,
) {
return <Primitive.Separator className="my-1 h-px bg-border" {...props} />;
}
export function ContextMenuSubTrigger({
children,
...props
}: ComponentProps<typeof Primitive.SubTrigger>) {
return (
<Primitive.SubTrigger
className="flex items-center justify-between gap-4 rounded-md px-2 py-2 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent"
{...props}
>
{children}
<ChevronRight className="size-3 rtl:rotate-180" />
</Primitive.SubTrigger>
);
}
export function ContextMenuSubContent({
className,
...props
}: ComponentProps<typeof Primitive.SubContent>) {
return (
<Primitive.Portal>
<Primitive.SubContent
className={cn(
"z-50 min-w-40 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg",
className,
)}
{...props}
/>
</Primitive.Portal>
);
}Required helper: src/lib/use-document-direction.ts
Copy this helper alongside the component. It keeps keyboard navigation aligned with the page direction and supports live RTL changes.
"use client";
import { useSyncExternalStore } from "react";
function subscribe(notify: () => void) {
const observer = new MutationObserver(notify);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["dir"],
});
return () => observer.disconnect();
}
function snapshot(): "ltr" | "rtl" {
return document.documentElement.dir === "rtl" ? "rtl" : "ltr";
}
/** Keep headless controls aligned with the document, including live RTL changes. */
export function useDocumentDirection() {
return useSyncExternalStore(subscribe, snapshot, () => "ltr" as const);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| onOpenChange (ContextMenu) | (open: boolean) => voidDefault: — | Observe menu visibility. |
| onSelect (ContextMenuItem) | (event: Event) => voidDefault: — | Handle an action. |
| checked / onCheckedChange | boolean | indeterminate / callbackDefault: — | Control a checkbox item. |
| destructive (ContextMenuItem) | booleanDefault: false | Use destructive text styling. |
| disabled | booleanDefault: false | Disable a target or item. |