React Context Menu --- 轻量级零依赖右键菜单组件
前言
在 Web 应用中,右键菜单(Context Menu)是一个常见但容易被忽视的交互细节。文件管理器里的「新建 / 重命名 / 删除」、编辑器里的「复制 / 粘贴」、表格里的「导出 / 打印」------ 右键菜单让用户能快速触达高频操作,大幅提升操作效率。
然而在 React 生态中,实现一个好用、可定制、无冗余依赖的右键菜单并不简单。要么引入笨重的 UI 组件库(Ant Design、Material UI),要么自己手写一堆重复的定位、防溢出、关闭逻辑。
今天我分享一个自己打磨的轻量方案 ------ @newnpmjs/react-context-menu。
设计理念
这个组件的核心设计思路只有三个:
- 零外部依赖 ------ 只依赖 React(≥16.8),没有
popper.js、没有floating-ui、没有styled-components - Provider 全局模式 ------ 一次包裹,全局可用,不需要在每个用到菜单的地方引入独立组件
- 三级自定义样式 ------ 整体菜单、子菜单层、单个菜单项,每一层都可以自由覆盖样式
安装
bash
npm install @newnpmjs/react-context-menu
仅需 React ≥ 16.8,没有其他运行时依赖。
三分钟快速上手
1. 包裹根组件
tsx
import { ContextMenuProvider } from "@newnpmjs/react-context-menu";
function App() {
return (
<ContextMenuProvider>
<MyComponent />
</ContextMenuProvider>
);
}
2. 在任意组件中使用
tsx
import { useContextMenu } from "@newnpmjs/react-context-menu";
function MyComponent() {
const { openContextMenu } = useContextMenu();
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
openContextMenu(e.clientX, e.clientY, [
{
key: "edit",
name: "编辑",
show: true,
onClick: () => alert("编辑"),
},
{
key: "copy",
name: "复制",
icon: "📋",
keyboard: "Ctrl+C",
show: true,
onClick: () => alert("复制"),
},
{ key: "sep1", type: "divider" },
{
key: "delete",
name: "删除",
disabled: true,
show: true,
},
]);
};
return <div onContextMenu={handleContextMenu}>右键点击我</div>;
}
效果
右键点击后,菜单会出现在鼠标位置,同时展示暗色主题、子菜单和危险项样式:

暗色主题菜单:整体深色背景 + 危险操作红色 Delete 项

二级子菜单:暗色主题的子菜单层同样覆盖了样式

per-call 独立样式:Log 区域菜单用了不同的深色背景和字号
- 支持分割线 (
type: "divider") - 支持禁用态 (
disabled: true) - 支持快捷键提示 (
keyboard) - 支持 Emoji / 图片 / React 元素图标 (
icon)
API 详解
ContextMenuProvider
包裹整个应用,管理菜单状态。
tsx
<ContextMenuProvider
menuClassName="dark-menu"
menuStyle={{ background: "#1e293b", border: "1px solid #334155" }}
>
<App />
</ContextMenuProvider>
| 参数 | 类型 | 说明 |
|---|---|---|
menuClassName |
string |
整体菜单的自定义 class |
menuStyle |
CSSProperties |
整体菜单的内联样式 |
useContextMenu()
返回两个方法:
ts
const { openContextMenu, closeContextMenu } = useContextMenu();
| 方法 | 签名 | 说明 |
|---|---|---|
openContextMenu |
(x, y, menus, options?) => void |
在指定坐标打开菜单 |
closeContextMenu |
() => void |
编程关闭菜单 |
openContextMenu 参数
ts
openContextMenu(
x: number, // 鼠标 X 坐标
y: number, // 鼠标 Y 坐标
menus: MenuItem[], // 菜单项数组
options?: { // 可选配置
width?: number;
menuClassName?: string;
menuStyle?: CSSProperties;
}
);
MenuItem 完整类型
ts
interface BaseMenuItem {
key: string;
name?: string;
onClick?: () => void;
disabled?: boolean;
icon?: string | ReactNode;
keyboard?: string;
// 自定义样式
itemClassName?: string;
itemStyle?: CSSProperties;
submenuClassName?: string;
submenuStyle?: CSSProperties;
}
interface MenuItemMenu extends BaseMenuItem {
type?: "menu";
show: boolean; // 控制显隐
children?: MenuItem[]; // 子菜单
}
interface MenuItemDivider {
type: "divider";
key: string;
}
| 字段 | 类型 | 适用 | 说明 |
|---|---|---|---|
key |
string |
全部 | 唯一标识 |
name |
string |
菜单 | 显示文字 |
show |
boolean |
菜单 | 是否显示 |
disabled |
boolean |
菜单 | 禁用态 |
icon |
`string | ReactNode` | 菜单 |
keyboard |
string |
菜单 | 快捷键提示 |
onClick |
() => void |
菜单 | 点击回调 |
children |
MenuItem[] |
菜单 | 子菜单 |
itemClassName |
string |
菜单 | 菜单项自定义 class |
itemStyle |
CSSProperties |
菜单 | 菜单项内联样式 |
submenuClassName |
string |
菜单 | 子菜单层自定义 class |
submenuStyle |
CSSProperties |
菜单 | 子菜单层内联样式 |
子菜单
嵌套 children 即可实现多级菜单:
tsx
openContextMenu(e.clientX, e.clientY, [
{
key: "new",
name: "新建",
show: true,
children: [
{ key: "file", name: "文件", show: true, onClick: () => createFile() },
{ key: "folder", name: "文件夹", show: true, onClick: () => createFolder() },
],
},
{ key: "sep", type: "divider" },
{
key: "sort",
name: "排序",
show: true,
children: [
{
key: "sort-by",
name: "按...排序",
show: true,
children: [
{ key: "name", name: "名称", show: true },
{ key: "date", name: "日期", show: true },
{ key: "size", name: "大小", show: true },
],
},
],
},
]);
子菜单的特性:
- 智能防溢出 ------ 右侧空间不足时会自动翻转到左侧
- 底部防溢出 ------ 超出视口底部时会自动上移
- 悬停延迟 ------ 150ms 延迟打开子菜单,防止误触
- 层级不限 ------ 理论上支持任意层级的嵌套
三级自定义样式
这是本组件最灵活的地方 ------ 你可以在三个维度自由控制样式。
第一级:整体菜单样式
通过 ContextMenuProvider 设置全局默认值:
tsx
<ContextMenuProvider
menuClassName="dark-menu"
menuStyle={{ background: "#1e293b", borderRadius: 8 }}
>
也可以在某次调用时临时覆盖:
tsx
openContextMenu(x, y, items, {
menuClassName: "danger-menu",
menuStyle: { background: "#450a0a" },
});
Provider 的样式作为默认值,per-call 的样式会覆盖上去。
第二级:子菜单层样式
每个菜单项可以单独控制其子菜单的样式:
tsx
{
key: "share",
name: "分享",
show: true,
submenuClassName: "share-submenu",
submenuStyle: {
minWidth: 200,
background: "#0f172a",
border: "1px solid #334155",
},
children: [
{ key: "wechat", name: "微信", show: true },
{ key: "weibo", name: "微博", show: true },
],
}
第三级:单个菜单项样式
tsx
{
key: "delete",
name: "删除",
show: true,
itemClassName: "danger-item",
itemStyle: { color: "#ef4444", fontWeight: 600 },
onClick: () => handleDelete(),
}
CSS 样式示例 ------ 暗色主题
css
/* 整体菜单暗色主题 */
.dark-menu .rcm-item {
color: #e2e8f0;
}
.dark-menu .rcm-item:hover {
background: #334155;
}
.dark-menu .rcm-item:active,
.dark-menu .rcm-item-active {
background: #475569;
}
.dark-menu .rcm-item.rcm-disabled {
color: #64748b;
}
.dark-menu .rcm-item-keyboard {
color: #64748b;
}
/* 子菜单暗色主题 */
.share-submenu {
background: #1e293b !important;
border-color: #334155 !important;
}
.share-submenu .rcm-item {
color: #e2e8f0;
}
.share-submenu .rcm-item:hover {
background: #334155;
}
/* 危险操作项样式 */
.danger-item:hover {
background: #fef2f2 !important;
}
.danger-item:active {
background: #fee2e2 !important;
}
传递给
className的 CSS 优先级高于默认注入的.rcm-*类,但低于style内联样式。如果你在 CSS 中需要强制覆盖,加上!important即可。
内置特性一览
✅ 零依赖
不像其他菜单库依赖 popper.js、react-popper、floating-ui 等第三方定位库,这个组件纯 React 实现定位逻辑,唯一的外部依赖就是 React 本身。
✅ 智能图标对齐
tsx
// 同一级菜单里,只要有一个项有 icon,所有项都会留出图标位置
// 如果同级菜单全都没有 icon,图标占位自动隐藏,菜单更紧凑
[
{ key: "a", name: "无图标", show: true }, // 不显示图标占位
{ key: "b", name: "有图标", icon: "📁", show: true }, // 显示图标占位
]
✅ 防溢出
菜单会自动检测视口边界:
- 右侧超出 → 菜单向左偏移
- 底部超出 → 菜单向上偏移
- 子菜单右侧超出 → 翻转到父菜单左侧
✅ 自动关闭
- 点击菜单外部 → 关闭
- 页面滚动 → 关闭
- 窗口失焦 → 关闭
- 通过
closeContextMenu()编程关闭
✅ CSS 自注入
样式在首次渲染时通过 useEffect 注入一次,不需要额外引入 CSS 文件,也不需要 CSS-in-JS 方案。
✅ 动画
菜单打开时有淡入 + 缩放动画(0.12s ease-out),可以通过自定义 CSS 覆盖。
完整 Demo
以下是一个完整的功能型 Demo,展示了暗色主题 + 子菜单 + 危险项样式 + per-call 样式覆盖:
tsx
import { ContextMenuProvider, useContextMenu } from "@newnpmjs/react-context-menu";
function App() {
return (
<ContextMenuProvider
menuClassName="dark-menu"
menuStyle={{ background: "#1e293b", border: "1px solid #334155" }}
>
<Page />
</ContextMenuProvider>
);
}
function Page() {
const { openContextMenu } = useContextMenu();
const onContext = (e: React.MouseEvent) => {
e.preventDefault();
openContextMenu(e.clientX, e.clientY, [
{
key: "new",
name: "新建",
show: true,
submenuClassName: "dark-submenu",
children: [
{ key: "file", name: "文件", show: true },
{ key: "folder", name: "文件夹", show: true },
],
},
{ key: "sep", type: "divider" },
{
key: "rename", name: "重命名", show: true,
icon: "✏️",
onClick: () => alert("重命名"),
},
{
key: "copy", name: "复制", show: true,
icon: "📋", keyboard: "Ctrl+C",
onClick: () => alert("复制"),
},
{ key: "sep", type: "divider" },
{
key: "delete", name: "删除", show: true,
icon: "🗑️", keyboard: "Del",
itemClassName: "danger-item",
itemStyle: { color: "#ef4444" },
onClick: () => alert("删除"),
},
]);
};
return <div onContextMenu={onContext}>右键点击此处</div>;
}
搭配 CSS:
css
.dark-menu .rcm-item { color: #e2e8f0; }
.dark-menu .rcm-item:hover { background: #334155; }
.dark-submenu { background: #1e293b !important; }
.danger-item:hover { background: #fef2f2 !important; }
和社区方案对比
| 特性 | @newnpmjs/react-context-menu |
react-contexify |
@radix-ui/react-context-menu |
|---|---|---|---|
| 依赖大小 | 零外部依赖 | ~15kB gzipped | ~30kB gzipped |
| Provider 模式 | ✅ | ✅ | ❌ 需独立组件 |
| 三级自定义样式 | ✅ | ❌ 仅整体样式 | ✅ |
| 子菜单防溢出 | ✅ | ✅ | ✅ |
| 键盘导航 | ❌ | ✅ | ✅ |
| 动画控制 | 基础 | 完整 | 完整 |
| 学习成本 | 极低 | 中等 | 较高 |
如果你只需要一个简单、轻量、无侵入的右键菜单,这个组件是最直接的选择。如果需要复杂的键盘导航和动画编排,社区其他库可能更合适。
总结
@newnpmjs/react-context-menu 是一个专注做好一件事的轻量组件:
- 极致轻量 ------ 不引入任何外部依赖
- API 简洁 ------ Provider + Hook 模式,三行代码接入
- 样式灵活 ------ 三级自定义覆盖,从整体到每个菜单项都可控
- 体验完善 ------ 防溢出、自动关闭、子菜单延迟、智能图标对齐
bash
npm install @newnpmjs/react-context-menu
🚀 在线体验 :react-context-menu-henna.vercel.app
右键点击页面任意区域,即可看到暗色主题菜单、子菜单和自定义样式的效果。
GitHub:github.com/awyb/react-context-menu
欢迎 Star、Issue、PR !如果你在项目中使用了,有任何反馈或建议都可以提出来。如果觉得有用,也欢迎分享给更多需要的朋友。