|$ curl https://forge-ai.dev/api/markdown?path=docs/components/context-menu
$cat docs/context-menu.md
updated Recently·25 min read·published

Context Menu

UI ComponentsIntermediate🎯Free Tools
Introduction

Context menus appear on right-click or long-press, providing context-specific actions. They must handle positioning (stay within viewport), keyboard access (Shift+F10, Menu key), nested submenus, and focus management.

Basic Context Menu
ContextMenu.tsx
TypeScript
1"use client";
2import { useState, useRef, useEffect, useCallback } from "react";
3
4interface MenuItem {
5 label: string;
6 icon?: string;
7 shortcut?: string;
8 action: () => void;
9 divider?: boolean;
10 disabled?: boolean;
11}
12
13function ContextMenu({ items }: { items: MenuItem[] }) {
14 const [position, setPosition] = useState({ x: 0, y: 0 });
15 const [isOpen, setIsOpen] = useState(false);
16 const menuRef = useRef<HTMLDivElement>(null);
17
18 const handleContextMenu = useCallback((e: React.MouseEvent) => {
19 e.preventDefault();
20 const x = Math.min(e.clientX, window.innerWidth - 200);
21 const y = Math.min(e.clientY, window.innerHeight - items.length * 36);
22 setPosition({ x, y });
23 setIsOpen(true);
24 }, [items.length]);
25
26 useEffect(() => {
27 if (!isOpen) return;
28 const handleClick = () => setIsOpen(false);
29 const handleKeyDown = (e: KeyboardEvent) => {
30 if (e.key === "Escape") setIsOpen(false);
31 };
32 document.addEventListener("click", handleClick);
33 document.addEventListener("keydown", handleKeyDown);
34 return () => {
35 document.removeEventListener("click", handleClick);
36 document.removeEventListener("keydown", handleKeyDown);
37 };
38 }, [isOpen]);
39
40 return (
41 <div onContextMenu={handleContextMenu}>
42 <div>Right-click here for actions</div>
43 {isOpen && (
44 <div
45 ref={menuRef}
46 role="menu"
47 className="fixed z-50 bg-[#1a1a28] border border-[#333] rounded-lg shadow-xl py-1 min-w-[200px]"
48 style={{ left: position.x, top: position.y }}
49 >
50 {items.map((item, i) =>
51 item.divider ? (
52 <div key={i} className="border-t border-[#333] my-1" />
53 ) : (
54 <button
55 key={i}
56 role="menuitem"
57 disabled={item.disabled}
58 className="w-full px-3 py-2 text-left text-sm text-[#E0E0E0] hover:bg-white/10 flex items-center justify-between disabled:opacity-40"
59 onClick={() => { item.action(); setIsOpen(false); }}
60 >
61 <span>{item.label}</span>
62 {item.shortcut && <span className="text-[#525252] text-xs">{item.shortcut}</span>}
63 </button>
64 )
65 )}
66 </div>
67 )}
68 </div>
69 );
70}

info

Always prevent the default browser context menu with e.preventDefault(). Otherwise both your menu and the native menu will appear.
Smart Positioning
positioning.ts
TypeScript
1// Keep menu within viewport bounds
2function getContextMenuPosition(
3 x: number,
4 y: number,
5 menuWidth: number,
6 menuHeight: number,
7) {
8 const padding = 8;
9 return {
10 x: Math.min(x, window.innerWidth - menuWidth - padding),
11 y: Math.min(y, window.innerHeight - menuHeight - padding),
12 };
13}
14
15// Keyboard trigger: Shift+F10 or Menu key
16// Find target element and show menu at its center
17function handleKeyboardTrigger(target: HTMLElement) {
18 const rect = target.getBoundingClientRect();
19 const x = rect.left + rect.width / 2;
20 const y = rect.top + rect.height / 2;
21 showContextMenu(x, y);
22}
$Blueprint — Engineering Documentation·Section ID: UI-CM-01·Revision: 1.0