Install it in one line
Available in v1.9.0
$npx devignerui add context-menu Writes components/ui/context-menu.tsx and lists what it needs.
First time here
Or by hand
- Needs React 18 or 19 and Tailwind CSS v4.
Install the dependencies.
$npm i cn motion react-dom @devigner-ui/icons devignerui - Paste the source from into
components/ui/context-menu.tsx.
Some of these designs take cues from interfaces we've admired around the web. If one started as your idea and you'd like credit, message us on X @devignerui or in the Discord and we'll add it.
How to use it
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.
Keyboard
- 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.
Stuck on something else? Ask in the Discord.