React Context Menu — 轻量级零依赖右键菜单组件

前言

在 Web 应用中,右键菜单(Context Menu)是一个常见但容易被忽视的交互细节。文件管理器里的「新建 / 重命名 / 删除」、编辑器里的「复制 / 粘贴」、表格里的「导出 / 打印」------ 右键菜单让用户能快速触达高频操作,大幅提升操作效率。

然而在 React 生态中,实现一个好用、可定制、无冗余依赖的右键菜单并不简单。要么引入笨重的 UI 组件库(Ant Design、Material UI),要么自己手写一堆重复的定位、防溢出、关闭逻辑。

今天我分享一个自己打磨的轻量方案 ------ @newnpmjs/react-context-menu


设计理念

这个组件的核心设计思路只有三个:

  1. 零外部依赖 ------ 只依赖 React(≥16.8),没有 popper.js、没有 floating-ui、没有 styled-components
  2. Provider 全局模式 ------ 一次包裹,全局可用,不需要在每个用到菜单的地方引入独立组件
  3. 三级自定义样式 ------ 整体菜单、子菜单层、单个菜单项,每一层都可以自由覆盖样式

安装

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;
  }
);
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.jsreact-popperfloating-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 !如果你在项目中使用了,有任何反馈或建议都可以提出来。如果觉得有用,也欢迎分享给更多需要的朋友。

相关推荐
Richown2 天前
用 Three.js + React 打造一个赛博朋克风格的 3D 作品集页面
区块链·react
qcx234 天前
【系统学AI】08 Plan-then-Execute范式:先想好再做,比ReAct强在哪
前端·人工智能·react.js·ai·react·plan execute
JaydenAI5 天前
[MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣
ai·人机交互·agent·react·hitl·maf·chatclient中间件
花月C7 天前
LangGraph 状态机与 ReAct Agent
python·agent·react·langgragh
Richown8 天前
密码学入门:区块链中的密码学原理
区块链·react
Richown8 天前
Web3钱包:钱包集成与签名验证
区块链·react
Richown8 天前
跨链桥接:多链资产转移实现
区块链·react
Richown8 天前
区块链开发:智能合约测试与调试技巧
区块链·react
Richown9 天前
物联网开发:MQTT与传感器数据采集
区块链·react