Skip to content

ContextMenu

Right-click anywhere on the desktop background and a system menu pops up. Right-click a window’s title bar: window menu. Right-click a dock tile: app menu. Right-click an item in the FileExplorer: item actions. Every one of those uses the same <ContextMenu> primitive and the same openContextMenu(...) store, so consumers wiring a custom region get the same look, the same keyboard behavior, and the same flip-to-stay-in-viewport positioning.

Right-click the desktop background, a dock tile, or a window's title bar Open in new tab ↗
import {
ContextMenu,
ContextMenuAnchor,
openContextMenu,
closeContextMenu,
type ContextMenuItem,
} from "@react-ui-os/desktop";

<Desktop> mounts <ContextMenu /> and <DesktopBackdrop /> for you. Mount them yourself only when composing your own layout from <DesktopProvider>.

The cleanest path when you already have the event in hand and the items depend on click state.

function MyRegion() {
return (
<div
onContextMenu={(e) => {
e.preventDefault();
openContextMenu({
x: e.clientX,
y: e.clientY,
ariaLabel: "Layer menu",
items: [
{ label: "Duplicate", shortcut: "⌘D", onSelect: duplicateLayer },
{ label: "Group", shortcut: "⌘G", onSelect: groupLayers },
{ separator: true },
{ label: "Delete", danger: true, onSelect: deleteLayer },
],
});
}}
>
...
</div>
);
}

Wraps a single child. The wrapped element gets an onContextMenu handler that opens the menu with the supplied items. Pass a function for items if they depend on per-event state.

<ContextMenuAnchor
items={[
{ label: "Open", onSelect: open, shortcut: "" },
{ label: "Rename", onSelect: rename, shortcut: "F2" },
{ separator: true },
{ label: "Delete", onSelect: del, danger: true, shortcut: "" },
]}
>
<div>Right-click me</div>
</ContextMenuAnchor>
FieldTypeDescription
labelstringDisplayed text. Omit when separator: true.
iconReactNodeOptional leading icon (~14 px).
shortcutstringRight-aligned hint ("⌘N", "F2", "↵"). Decorative. Does not bind the key.
onSelect() => voidActivation handler. The menu closes after this runs.
disabledbooleanGreys out the row and skips it in keyboard navigation.
dangerbooleanTints the row red. Typical pattern: sits below a separator (Delete, Move to Trash).
separatorbooleanRenders a divider instead of a row. The other fields are ignored.
submenuContextMenuItem[]Reserved for nested menus (not yet implemented in the renderer).
KeyEffect
↑ / ↓Move highlight. Wraps; skips disabled rows.
Enter / SpaceActivate the highlighted row.
EscClose. Focus returns to whatever was focused before.

Right-clicking the desktop background opens a built-in menu via <DesktopBackdrop>. Customize it without rebuilding the rest:

import { DesktopBackdrop, type ContextMenuItem } from "@react-ui-os/desktop";
const extraItems: ContextMenuItem[] = [
{ label: "Change wallpaper…", onSelect: openWallpaperPicker },
{ label: "Sort icons", onSelect: sortDesktopIcons },
];
<DesktopProvider apps={apps} theme={theme}>
<Wallpaper />
<MenuBar />
<WindowLayer />
<Dock />
<DesktopBackdrop extraItems={extraItems} />
{/* ... */}
</DesktopProvider>;

For full control, pass buildItems instead; it receives the defaults so you can keep, drop, or reorder them.

Window title bars and dock tiles each get their own context menu out of the box (Restore/Minimize/Close on the title bar, Open/Minimize/Close/Mark-as-read on the dock tile). Override or extend by registering app-specific items inside your content component; the same openContextMenu is available there.

  • The menu surface is role="menu" with the ariaLabel you supplied.
  • Each row is role="menuitem".
  • Separators are role="separator".
  • Focus is restored to the element that opened the menu on close (when the consumer passes returnFocusTo, which ContextMenuAnchor does automatically).
  • <Desktop>: mounts both the renderer and the backdrop.
  • Notifications: same pattern (vanilla store + mounted component + imperative call from anywhere).