Skip to content
WANCRAFT

D.107Navigation & Menus

Context Menu

A right-click and long-press menu with checkbox and radio items, nested submenus, shortcuts, typeahead and full menu ARIA.

NewReactMotionTailwindadvanced

Overview

Context Menu turns any area into a target for a custom menu. Right-click, a touch long-press, Shift+F10 or the ContextMenu key opens it at the pointer (or at the focused element), in the top layer so it is never clipped, flipped and clamped to stay inside the viewport and scaled in on a spring from the exact point you clicked. The menu is data-driven: actions with icons and shortcut hints, destructive items, checkbox items, radio groups, labelled groups, separators and submenus nested to any depth, which open on hover or ArrowRight and flip to the other side near the edge. Arrow keys, Home / End, typeahead, Enter / Space, ArrowLeft, Escape and Tab all behave like a native menu, and focus returns to the target when it closes.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/context-menu.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/context-menu.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/context-menu.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

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

export function ContextMenuExample() {
  const items: ContextMenuEntry[] = [
    { id: "copy", label: "Copy", shortcut: "⌘C", onSelect: () => console.log("copy") },
    { id: "paste", label: "Paste", shortcut: "⌘V" },
    { type: "separator" },
    {
      type: "submenu",
      id: "share",
      label: "Share",
      items: [
        { id: "link", label: "Copy link" },
        { id: "email", label: "Email" },
      ],
    },
    { type: "separator" },
    { id: "delete", label: "Delete", destructive: true },
  ];

  return (
    <ContextMenu
      items={items}
      className="grid h-48 place-items-center rounded-xl border border-dashed"
    >
      Right-click me
    </ContextMenu>
  );
}

03Props

PropTypeDefaultDescription
itemsContextMenuEntry[]—Required. Actions { id, label, icon?, shortcut?, disabled?, destructive?, onSelect? }, { type: "checkbox", checked, onCheckedChange }, { type: "radio", value, onValueChange, options }, { type: "group", label, items }, { type: "submenu", label, items } and { type: "separator" }.
childrenReactNode—Required. Content of the target area.
onSelect(id: string) => void—Called for every activated action, checkbox and radio option (radio ids are "groupId:value").
widthnumber224Minimum menu width in px.
longPressDelaynumber500Touch / pen hold before the menu opens, in ms (cancelled by moving more than 10px).
showShortcutsbooleantrueShow shortcut hints (display only).
spring{ stiffness; damping; mass }{ 520, 32, 0.7 }Spring for the scale-in from the pointer.
accentstring"#FF7448"Highlighted item tint, check marks and focus ring.
menuLabelstring"Context menu"Accessible name of the menu.
disabledbooleanfalseTurn the custom menu off (the browser's menu shows).
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the target element (className, aria-label…).

04Features

  • 01Opens on right-click, touch long-press, Shift+F10 and the ContextMenu key
  • 02Top-layer panel (Popover API), flipped and clamped to the viewport, scaling in from the pointer on a spring
  • 03Actions, destructive items, checkbox items, radio groups, labelled groups and separators
  • 04Submenus nested to any depth, opening on hover (with intent delay) or ArrowRight and flipping near the edge
  • 05Shortcut hints, typeahead, disabled items skipped by the keyboard
  • 06Checkbox and radio items keep the menu open so several can be toggled in a row
motion · springinput · clickinput · pressinput · keyboardinput · hoverinput · pointer

05Accessibility

  • role="menu" panels with menuitem, menuitemcheckbox and menuitemradio items (aria-checked), role="group" with labels, and role="separator".
  • Submenu triggers expose aria-haspopup="menu", aria-expanded and aria-controls; submenus are labelled by their trigger.
  • Keyboard: Arrow keys, Home / End, typeahead, Enter / Space, ArrowRight / ArrowLeft for submenus, Escape and Tab close and return focus to the target.
  • The target is focusable, announces aria-haspopup / aria-expanded and describes how to open the menu (Shift+F10).
  • Shortcut hints and icons are aria-hidden; with prefers-reduced-motion the menu only fades.

06Limitations

  • Shortcut hints are display only; bind the keyboard shortcuts in your app.
  • Without Popover API support the menu falls back to position: fixed, which transformed ancestors can offset.