The menu that opens at the pointer on a right click, a long press or the keyboard (Shift+F10 or the Menu key) over an area such as a file row. It never replaces a visible way to do the same thing.
Right click the file, or focus it and press Shift+F10.
import { useState } from "react";
import { Button, ContextMenu, ContextMenuItem, CopyIcon, FileIcon } from "@rdloom/react";
// The same actions are in the visible "More" menu of a real list; the right click is a shortcut.
export default function ContextMenuBasicExample() {
const [last, setLast] = useState<string>();
return (
<div className="flex flex-col items-center gap-3 p-6">
<ContextMenu
label="Actions for report.pdf"
onAction={(key) => setLast(String(key))}
className="flex w-72 items-center gap-3 rounded-[var(--rd-radius-control)] border border-[var(--rd-color-border-default)] px-3 py-2.5 text-sm"
items={
<>
<ContextMenuItem id="open" icon={<FileIcon className="size-4" />}>
Open
</ContextMenuItem>
<ContextMenuItem id="copy" icon={<CopyIcon />}>
Copy link
</ContextMenuItem>
<ContextMenuItem id="rename">Rename</ContextMenuItem>
</>
}
>
<FileIcon className="size-5" />
<span className="flex-1">report.pdf</span>
<Button variant="ghost" size="sm" onPress={() => setLast("open")}>
Open
</Button>
</ContextMenu>
<p className="text-xs text-[var(--rd-color-text-muted)]">Right click the file, or focus it and press Shift+F10.</p>
<p className="text-sm" aria-live="polite">
{last ? `Chose ${last}` : ""}
</p>
</div>
);
}Installation
npx rdloom add context-menuCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/context-menu.jsonWorks, but without upgrade tracking.
Usage
import { Button, ContextMenu, ContextMenuItem, CopyIcon, FileIcon } from "@rdloom/react";
<div className="flex flex-col items-center gap-3 p-6">
<ContextMenu
label="Actions for report.pdf"
onAction={(key) => setLast(String(key))}
className="flex w-72 items-center gap-3 rounded-[var(--rd-radius-control)] border border-[var(--rd-color-border-default)] px-3 py-2.5 text-sm"
items={
<>
<ContextMenuItem id="open" icon={<FileIcon className="size-4" />}>
Open
</ContextMenuItem>
<ContextMenuItem id="copy" icon={<CopyIcon />}>
Copy link
</ContextMenuItem>
<ContextMenuItem id="rename">Rename</ContextMenuItem>
</>
}
>
<FileIcon className="size-5" />
<span className="flex-1">report.pdf</span>
<Button variant="ghost" size="sm" onPress={() => setLast("open")}>
Open
</Button>
</ContextMenu>
<p className="text-xs text-[var(--rd-color-text-muted)]">Right click the file, or focus it and press Shift+F10.</p>
<p className="text-sm" aria-live="polite">
{last ? `Chose ${last}` : ""}
</p>
</div>With sections and shortcuts
import { ContextMenu, ContextMenuItem, ContextMenuSection, ContextMenuSeparator, CopyIcon, FileIcon, PinIcon } from "@rdloom/react";
export default function ContextMenuWithSectionsAndShortcutsExample() {
return (
<div className="flex min-w-0 max-w-full justify-center p-6">
<ContextMenu
label="Actions for Q3 plan"
className="flex w-80 max-w-full flex-col gap-1 rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] p-4 text-sm"
items={
<>
<ContextMenuSection title="Open">
<ContextMenuItem id="open" icon={<FileIcon className="size-4" />} shortcut="Enter">
Open
</ContextMenuItem>
<ContextMenuItem id="copy" icon={<CopyIcon />} shortcut="⌘C">
Copy link
</ContextMenuItem>
<ContextMenuItem id="pin" icon={<PinIcon />}>
Pin to top
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuSeparator />
<ContextMenuSection title="Manage">
<ContextMenuItem id="rename" shortcut="F2">
Rename
</ContextMenuItem>
<ContextMenuItem id="archive" isDisabled>
Archive (needs approval)
</ContextMenuItem>
<ContextMenuItem id="delete" variant="danger" shortcut="⌫">
Delete plan
</ContextMenuItem>
</ContextMenuSection>
</>
}
>
<p className="font-semibold">Q3 plan</p>
<p className="text-[var(--rd-color-text-muted)]">Right click, long press, or press Shift+F10 when this card is focused.</p>
</ContextMenu>
</div>
);
}On a table row
| Customer | Total | Details |
|---|---|---|
| Ada Lovelace | $120.50 | Open inv-1042 |
| Grace Hopper | $89.00 | Open inv-1043 |
| Alan Turing | $42.10 | Open inv-1044 |
import { useState } from "react";
import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@rdloom/react";
const rows = [
{ id: "inv-1042", customer: "Ada Lovelace", total: "$120.50" },
{ id: "inv-1043", customer: "Grace Hopper", total: "$89.00" },
{ id: "inv-1044", customer: "Alan Turing", total: "$42.10" },
];
// The row stays a table row (elementType="tr"). Each row also has a visible Open link,
// so the menu adds shortcuts and removes nothing.
export default function ContextMenuOnATableRowExample() {
const [last, setLast] = useState<string>();
return (
<div className="flex flex-col items-center min-w-0 max-w-full gap-3 p-6">
<table className="w-96 max-w-full border-collapse text-sm">
<caption className="pb-2 text-start text-[var(--rd-color-text-muted)]">Invoices</caption>
<thead>
<tr className="border-b border-[var(--rd-color-border-default)]">
<th scope="col" className="py-2 text-start font-medium">
Customer
</th>
<th scope="col" className="py-2 text-end font-medium">
Total
</th>
<th scope="col" className="py-2 ps-4 text-start font-medium">
Details
</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<ContextMenu
key={row.id}
elementType="tr"
label={`Actions for ${row.id}`}
onAction={(key) => setLast(`${key} on ${row.id}`)}
className="border-b border-[var(--rd-color-border-default)]"
items={
<>
<ContextMenuItem id="open">Open invoice</ContextMenuItem>
<ContextMenuItem id="send">Send reminder</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem id="void" variant="danger">
Void invoice
</ContextMenuItem>
</>
}
>
<td className="py-2">{row.customer}</td>
<td className="py-2 text-end tabular-nums">{row.total}</td>
<td className="py-2 ps-4">
<a href={`#${row.id}`} className="underline underline-offset-4 outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)]">
Open {row.id}
</a>
</td>
</ContextMenu>
))}
</tbody>
</table>
<p className="text-sm" aria-live="polite">
{last ?? ""}
</p>
</div>
);
}Disabled
import { ContextMenu, ContextMenuItem } from "@rdloom/react";
// While the file is locked the menu is off: the area is an ordinary element and the browser menu returns.
export default function ContextMenuDisabledExample() {
return (
<div className="flex justify-center p-6">
<ContextMenu
isDisabled
label="Actions for locked.pdf"
className="w-72 rounded-[var(--rd-radius-control)] border border-[var(--rd-color-border-default)] px-3 py-2.5 text-sm text-[var(--rd-color-text-muted)]"
items={<ContextMenuItem id="open">Open</ContextMenuItem>}
>
locked.pdf (read only, no menu)
</ContextMenu>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
labelrequiredAccessible name of the menu, e.g. "Actions for report.pdf". | string | none |
childrenrequiredThe area the menu belongs to, such as a row or a card. It becomes focusable. | node | none |
itemsrequiredContextMenuItem, ContextMenuSection and ContextMenuSeparator elements. | node | none |
hintText read by screen readers when the area is focused. It is not shown on screen. | string | "Press Shift+F10 for actions" |
onActionCalled with the id of the item chosen. | (key: Key) => void | none |
selectionModenone for actions; single or multiple makes every item checkable. | "none" | "single" | "multiple" | "none" |
selectedKeysControlled selected items when selectionMode is not none. | Selection | none |
defaultSelectedKeysInitially selected items when uncontrolled. | Selection | none |
onSelectionChangeCalled when the checked items change. | (keys: Selection) => void | none |
isDisabledTurns the menu off. The area stays as it is and the browser's own menu returns. | boolean | false |
elementTypeThe element the area renders as, so a table row stays a row. | "div" | "li" | "tr" | "article" | "section" | "div" |
Accessibility
Role menu, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Shift+F10 or the Menu key on the focused area opens the menu next to it
- Up and Down Arrow move between items, wrapping at the ends; Home and End jump to the first and last
- Typing a letter moves to the next item starting with it
- Enter or Space chooses the item and closes the menu (in a checkable menu, toggles it)
- Esc closes the menu and returns focus to the area
Screen readers announce
- Focusing the area reads its content followed by "Press Shift+F10 for actions"
- Opening announces a menu named by its label and its first item, with its position
- Closing returns to the area
What your code must do
- A context menu is never the only way to do something: every action must also exist as a visible control
- The area is focusable (tabIndex 0) and described by a hint, so keyboard and screen reader users learn the menu is there
- Opens on right click, on a touch long press, and from the keyboard
- Focus returns to the area when the menu closes
- Destructive items say what they do in their text, not only in red
Guidelines
Use it when
- Shortcuts to actions that already have a visible button or menu, on rows, cards or files
- Dense lists where every row cannot carry a row of buttons
Avoid it when
- The action has no other way to be reached: add a button or a Menu
- An action list on a button: use Menu
- Text fields: leave the browser's own menu (copy, paste)
Don't
- A context menu as the only place for an action
- Disabling the browser menu over a whole page
- Nesting a context menu inside another
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-raised--rd-color-surface-subtle--rd-color-border-default--rd-color-text-default--rd-color-text-muted--rd-color-feedback-danger--rd-color-focus-ring--rd-radius-overlay--rd-radius-control