@mshafiqyajid/react-context-menu
Right-click context menu for React. Headless hook and styled component. Items, separators, section labels, icons, keyboard shortcuts, nested sub-menus, ArrowKey + first-letter navigation, viewport collision detection, scale+fade animation from click origin. Portal to document.body, ARIA-compliant.
Playground #
Right-click anywhere in this area
Props
TSX
import { ContextMenuStyled } from "@mshafiqyajid/react-context-menu/styled";
import "@mshafiqyajid/react-context-menu/styles.css";
<ContextMenuStyled
items={items}
/>Install #
npm install @mshafiqyajid/react-context-menu Quick start #
import { ContextMenuStyled } from "@mshafiqyajid/react-context-menu/styled";
import "@mshafiqyajid/react-context-menu/styles.css";
<ContextMenuStyled
items={[
{ label: "Cut", shortcut: "⌘X", onClick: () => cut() },
{ label: "Copy", shortcut: "⌘C", onClick: () => copy() },
{ label: "Paste", shortcut: "⌘V", onClick: () => paste() },
{ type: "separator" },
{ label: "Delete", onClick: () => remove(), disabled: true },
]}
>
<div style={{ padding: "2rem", border: "1px dashed #ccc" }}>
Right-click anywhere in this area
</div>
</ContextMenuStyled> Sub-menus #
<ContextMenuStyled
items={[
{ label: "New File", onClick: () => createFile() },
{ label: "New Folder", onClick: () => createFolder() },
{ type: "separator" },
{
label: "Share",
items: [
{ label: "Copy link", onClick: () => copyLink() },
{ label: "Send by email", onClick: () => sendEmail() },
],
},
{ type: "separator" },
{ label: "Delete", onClick: () => deleteItem() },
]}
>
<div>Right-click me</div>
</ContextMenuStyled> Sub-menu rows show a chevron. Hover or press → to open. Press ← or Escape inside to close. Enter/Space activates.
Section labels #
<ContextMenuStyled
items={[
{ type: "label", label: "Edit" },
{ label: "Cut", onClick: () => cut() },
{ label: "Copy", onClick: () => copy() },
{ type: "separator" },
{ type: "label", label: "Clipboard" },
{ label: "Paste", onClick: () => paste() },
]}
>
<div>Right-click me</div>
</ContextMenuStyled> Controlled open state #
const [open, setOpen] = useState(false);
<ContextMenuStyled
items={items}
open={open}
onOpenChange={setOpen}
>
<div>Right-click me</div>
</ContextMenuStyled> Headless #
import { useContextMenu } from "@mshafiqyajid/react-context-menu";
import { createPortal } from "react-dom";
const items = ["Cut", "Copy", "Paste"];
function MyContextMenu() {
const { triggerProps, menuProps, getItemProps, isOpen } = useContextMenu({
itemCount: items.length,
});
return (
<>
<div {...triggerProps} style={{ padding: "2rem" }}>
Right-click here
</div>
{isOpen && createPortal(
<div {...menuProps} className="my-menu">
{items.map((label, i) => (
<div
key={label}
{...getItemProps(i, { onClick: () => console.log(label) })}
>
{label}
</div>
))}
</div>,
document.body,
)}
</>
);
} Keyboard navigation #
| Key | Action |
|---|---|
↓ / ↑ | Navigate items (skips disabled) |
Enter / Space | Activate focused item |
→ | Open sub-menu |
← / Escape (in sub-menu) | Close sub-menu, return focus to parent |
Escape | Close menu |
| Letter key | Jump to first item whose label starts with that letter |
API #
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | The right-click target area |
| items | ContextMenuItem[] | — | Menu items array |
| disabled | boolean | false | Disable the context menu entirely |
| open | boolean | — | Controlled open state |
| onOpenChange | (open: boolean) => void | — | Callback when open state changes |
| className | string | — | Class on the trigger wrapper |
| style | CSSProperties | — | Style on the trigger wrapper |
ContextMenuItem #
| Field | Type | Description |
|---|---|---|
| type | "item" | "separator" | "label" | Defaults to "item". "separator" renders a horizontal rule; "label" renders a non-interactive section heading. |
| label | string? | Text content |
| icon | ReactNode? | 16×16 icon rendered on the left |
| shortcut | string? | Right-aligned shortcut badge (display only) |
| disabled | boolean? | Dims item, prevents interaction and keyboard activation |
| onClick | () => void? | Callback fired on click or Enter/Space |
| items | ContextMenuItem[]? | Nested items — renders a sub-menu |