react ~4 KB 0 deps v0.1.0 ↗ GitHub ↗

@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>
<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 #

KeyAction
/ Navigate items (skips disabled)
Enter / SpaceActivate focused item
Open sub-menu
/ Escape (in sub-menu)Close sub-menu, return focus to parent
EscapeClose menu
Letter keyJump to first item whose label starts with that letter

API #

PropTypeDefaultDescription
childrenReactNodeThe right-click target area
itemsContextMenuItem[]Menu items array
disabledbooleanfalseDisable the context menu entirely
openbooleanControlled open state
onOpenChange(open: boolean) => voidCallback when open state changes
classNamestringClass on the trigger wrapper
styleCSSPropertiesStyle on the trigger wrapper

ContextMenuItem #

FieldTypeDescription
type"item" | "separator" | "label"Defaults to "item". "separator" renders a horizontal rule; "label" renders a non-interactive section heading.
labelstring?Text content
iconReactNode?16×16 icon rendered on the left
shortcutstring?Right-aligned shortcut badge (display only)
disabledboolean?Dims item, prevents interaction and keyboard activation
onClick() => void?Callback fired on click or Enter/Space
itemsContextMenuItem[]?Nested items — renders a sub-menu
Edit this page on GitHub