# Context Menu

> A right-click menu that opens where you click, stays inside the screen and slides into submenus.

- Page: https://ui.devigner.cc/components/context-menu
- Category: Interface
- Requirements: React 18 or 19, Tailwind CSS v4
- Install: `npx devignerui add context-menu` (writes components/ui/context-menu.tsx)
- Packages it needs: cn, motion, react-dom, @devigner-ui/icons, devignerui

## What the demo shows

The live demo is a file card. Right-click it, or hold it on a touch screen, and the menu opens at that spot with Open, Rename, Duplicate, Copy link, a Move to submenu and Delete; a chosen action shows underneath.

## When to use it

- Actions on an object people point at: files, rows, cards, canvas items.
- Power-user shortcuts that also live somewhere visible, such as a toolbar or a row menu.

## When to reach for something else

- The only way to reach an action. Right-click is hidden; give every action a visible route too.
- A menu opened by clicking a button. Use a dropdown or Menu Dock instead.

## Usage

```tsx
import { ContextMenu } from "@/components/ui/context-menu";

<ContextMenu items={actions}><FileCard /></ContextMenu>
```

Custom composition:

```tsx
<ContextMenu items={actions} label="File actions" onSelect={(item) => track(item.label)}><FileCard /></ContextMenu>
```

### Row actions with a submenu

Separators group rows; a row with items opens a submenu.

```tsx
import { ContextMenu, type ContextMenuEntry } from "@/components/ui/context-menu";

export function FileRow({ name, onDelete }: { name: string; onDelete: () => void }) {
  const items: ContextMenuEntry[] = [
    { label: "Open", shortcut: ["Enter"], onSelect: () => console.log("open", name) },
    {
      label: "Move to",
      items: [
        { label: "Inbox", onSelect: () => console.log("inbox") },
        { label: "Archive", onSelect: () => console.log("archive") },
      ],
    },
    "separator",
    { label: "Delete", destructive: true, onSelect: onDelete },
  ];
  return (
    <ContextMenu items={items} label={`Actions for ${name}`}>
      <div tabIndex={0} className="rounded-xl p-3">{name}</div>
    </ContextMenu>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| items | `ContextMenuEntry[]` | - | The rows: { label, icon?, shortcut?, items?, onSelect?, destructive?, disabled? }, or "separator" for a line between groups. A row with items opens a submenu. |
| children | `ReactNode` | - | The area that opens the menu on right-click, Shift+F10 or a long press. |
| onSelect | `(item) => void` | - | Called for every chosen row that is not a submenu, after the row's own onSelect. |
| onOpenChange | `(open: boolean) => void` | - | Called when the menu opens or closes. |
| label | `string` | Actions | Accessible name of the menu. |
| disabled | `boolean` | false | Leaves the browser's own right-click menu in place. |
| classNames | `ContextMenuClassNames` | - | Slot classes: menu (every surface, submenus included), item and indicator (the gliding highlight). |

## Keyboard

| Keys | Action |
| --- | --- |
| Shift+F10 / Menu key | Opens the menu under the focused element, with the first row highlighted. |
| Up / Down arrows | Move between rows, skipping disabled ones, looping at the ends. |
| Home / End | First or last row. |
| Right arrow / Enter | Opens a submenu and moves into it. |
| Left arrow | Closes a submenu and goes back to the row that opened it. |
| A letter | Jumps to the next row starting with it. |
| Escape | Closes a submenu, then the menu, and gives focus back. |
| Tab | Closes the menu. |

## Accessibility

- Each level is role="menu" with menuitem rows and separator lines. Rows with a submenu carry aria-haspopup and aria-expanded.
- Focus moves into the menu when it opens and returns to where it was when it closes after a choice or Escape.
- Disabled rows are skipped by the keys and can't be clicked.
- Make the area focusable (tabIndex={0} or a focusable child) so keyboard users can open it with Shift+F10.
- Honors prefers-reduced-motion and an ancestor <MotionConfig reducedMotion="always">, so an in-app motion switch works too.

## Theming

- classNames.menu styles every surface, submenus included; classNames.item and classNames.indicator style the rows and the gliding highlight.
- Colors come from shadcn tokens: the highlight is foreground at 4.5% (7% in dark), destructive rows use destructive. The surface is var(--panel) when your app defines it and background otherwise, with a 4% foreground rim and a layered shadow. Key caps are inset wells on background/40.
- shortcut shows key caps at the end of a row. They are display only; bind the keys yourself.

## Edge cases

- The menu renders in a portal on document.body with fixed positioning, so a clipping or scrolling parent never cuts it off.
- Near the right or bottom edge it flips to the other side of the pointer, and it always keeps 8px from the screen's edge. Submenus flip left when there is no room on the right.
- Scrolling, resizing, switching windows or a press outside closes it without moving focus.
- A long press opens it on touch screens, including iOS, which never fires contextmenu. Moving the finger more than 10px cancels it.
- An event handler on children that calls preventDefault on contextmenu stops the menu from opening.

## Troubleshooting

**The browser's own menu still opens.**
Check that disabled is off and that nothing inside children calls preventDefault on contextmenu, which tells the menu someone else handled it.

**Keyboard users can't open it.**
Shift+F10 works on the focused element, so the area needs something focusable: give it tabIndex={0} or put a button inside.

**The component renders with no background or the wrong colors.**
Colors use the standard shadcn/ui tokens only (background, foreground, primary, secondary, muted, accent, border, input, ring, destructive), so a project set up with shadcn/ui needs nothing extra and the component follows its theme, dark mode included. Without shadcn/ui, define those CSS variables for :root and .dark and map them in your Tailwind v4 @theme, or run npx shadcn init.
