Skip to content

Context Menu

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.

Overlayv0.1.0experimentalWCAG 2.2 AAView spec
report.pdf

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-menu

Copies 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.json

Works, 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

Q3 plan

Right click, long press, or press Shift+F10 when this card is focused.

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

Invoices
CustomerTotalDetails
Ada Lovelace$120.50Open inv-1042
Grace Hopper$89.00Open inv-1043
Alan Turing$42.10Open 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

locked.pdf (read only, no menu)
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.

PropTypeDefault
labelrequired

Accessible name of the menu, e.g. "Actions for report.pdf".

stringnone
childrenrequired

The area the menu belongs to, such as a row or a card. It becomes focusable.

nodenone
itemsrequired

ContextMenuItem, ContextMenuSection and ContextMenuSeparator elements.

nodenone
hint

Text read by screen readers when the area is focused. It is not shown on screen.

string"Press Shift+F10 for actions"
onAction

Called with the id of the item chosen.

(key: Key) => voidnone
selectionMode

none for actions; single or multiple makes every item checkable.

"none" | "single" | "multiple""none"
selectedKeys

Controlled selected items when selectionMode is not none.

Selectionnone
defaultSelectedKeys

Initially selected items when uncontrolled.

Selectionnone
onSelectionChange

Called when the checked items change.

(keys: Selection) => voidnone
isDisabled

Turns the menu off. The area stays as it is and the browser's own menu returns.

booleanfalse
elementType

The 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