下拉菜单与菜单系统
目的
下拉菜单与菜单系统技能提供了一个标准化的框架,用于在 Web 界面中设计浮动操作菜单、溢出菜单、上下文菜单和级联子菜单。下拉菜单以附加到触发元素(如图标按钮、拆分按钮或上下文操作)的方式,显示交互式命令、选项或子操作的有序列表。
设计一个有效的下拉菜单系统需要在空间布局、视觉层次、项目分组、键盘导航语义、碰撞处理(视口边界)以及在基于触摸的移动视口上进行响应式适配之间取得平衡,同时避免使用户迷失方向或违反 WCAG 2.1 AA 无障碍指南。
使用场景
- 操作与选项菜单: 在数据表格、卡片或页面标题中,显示附加到触发按钮的次要页面或行操作(例如“编辑”、“复制”、“导出”、“删除”)。
- 溢出与更多菜单: 当容器空间受限时(例如工具栏或表格标题中的“...”按钮),容纳优先级较低的工具或导航链接。
- 上下文弹出菜单: 在特定视觉画布或列表项上通过右键单击或长按触发,显示有针对性的上下文命令。
- 级联子菜单: 在多层级菜单覆盖层中组织嵌套操作或多级分类选项。
- 选择操作菜单(可勾选和单选项目): 在浮动操作面板中切换视图状态、列可见性或排序偏好。
何时不使用
- 表单选择输入(组合框与选择输入): 对于用户需选取一个或多个值以填充字段的表单输入(例如选择国家或产品类别),请使用
custom-select-and-combobox-system。 - 高密度导航面板: 对于包含结构化类别、登录页面和营销推广的多列全局站点导航,请使用
mega-menu-navigation-system。 - 大型模态与任务工作区: 对于需要保持焦点的复杂子任务、多字段编辑或多步向导表单,请使用
overlay-and-dialog-system或bottom-sheet-design-system。 - 上下文帮助与工具提示: 对于非交互式的悬停/聚焦文本描述(解释元素功能),请使用
tooltip-and-hint-system。
输入
- 触发组件与几何信息: 主要触发元素类型(标准按钮、图标按钮、拆分按钮或右键目标区域)及其空间定位。
- 菜单项清单与层级结构: 操作列表、项目类型(标准、可勾选、单选、破坏性)、图标、键盘快捷键、分隔符和分组节标签。
- 表面与高度 token: 背景表面颜色、边框样式、投影 token(
--elevation-popover)和 z-index 定位(--z-popover),源自elevation-and-depth-system和accessible-color-system。 - 视口与设备上下文: 设备方向、屏幕宽度断点和触控支持特性。
输出
- 菜单结构与布局蓝图: 用于菜单容器尺寸、内边距、项目对齐、尾部快捷键提示和节分隔符间距的 CSS 和 HTML 结构规范。
- 空间定位与碰撞策略: 关于锚点定位(顶部/底部/左侧/右侧)、对齐、动态翻转/滑动逻辑以及最大高度可滚动边界的布局规则。
- 响应式移动适配模型: 将窄屏桌面下拉弹出式菜单转换为移动视口上的底部动作面板(
bottom-sheet-design-system)或全宽触摸列表的设计规范。 - ARIA 与键盘导航规范: 涵盖 ARIA 角色(
role="menu"、role="menuitem"、role="menuitemcheckbox"、role="menuitemradio")、展开状态(aria-expanded)和箭头键焦点管理的可编程无障碍规则。
工作流程
1. 定义菜单结构与项目解剖
将菜单位按逻辑功能分组进行分类,以最大化可扫描性:
- 标题与节标签: 使用大写微排版或微妙的节标题(
font-size: 0.75rem、font-weight: 600、letter-spacing: 0.05em)对相关操作进行分组。 - 标准项目组成: 使用 flexbox(
gap: 0.75rem)水平对齐元素:- 前导图标: 16x16px 或 20x20px 的图标,用于阐明操作目的。
- 标签文本: 简洁、以操作为导向的动词(例如“下载 PDF”、“复制项目”)。
- 尾部快捷键提示: 等宽键盘快捷键文本(例如
⌘E、Ctrl+D),视觉上柔和处理(color: var(--text-tertiary))。 - 尾部箭头: 表示级联子菜单触发器(
>)。
- 专用项目类型:
- 可勾选项目(
role="menuitemcheckbox"): 激活时显示前导勾选标记。 - 单选项目(
role="menuitemradio"): 在组内选中时显示单选圆点。 - 破坏性项目: 使用独特的警告颜色 token(
color: var(--color-danger))对负面操作(例如“删除”、“移除”)进行样式设置,并通过视觉分隔线与安全操作分开。
- 可勾选项目(
- 分隔线(
role="separator"): 使用微妙的水平规则(1px solid var(--border-subtle))来分隔不同的操作组(例如将视图操作与编辑/删除操作分开)。
2. 建立空间定位与碰撞处理
配置布局规则,使菜单在其触发器附近清晰渲染,而不会超出屏幕边缘:
- 锚点对齐: 相对于触发器按钮边界定位菜单弹出框:
- 底部-左侧 / 底部-右侧: 触发器按钮下方的默认定位。当触发器靠近屏幕右边缘时(例如数据表格行操作列),向右边缘对齐。
- 顶部-左侧 / 顶部-右侧: 当靠近视口底部时,翻转方向至触发器按钮上方。
- 视口碰撞与翻转规则:
- 保持与屏幕边缘至少 12px 的边距(
viewport-margin: 12px)。 - 设置最大高度约束(
max-height: calc(100vh - 32px)),对于长菜单启用内部垂直滚动(overflow-y: auto)。
- 保持与屏幕边缘至少 12px 的边距(
- 高度 token: 应用顶层堆叠(
z-index: var(--z-popover))和投影(box-shadow: var(--shadow-popover)),在视觉上将浮动菜单与背景表格行或卡片区分开。
3. 配置交互与子菜单动态
- 触发器状态: 确保触发器按钮使用
aria-expanded="true"和微妙的按下视觉状态(background-color: var(--bg-hover))指示活动的打开状态。 - 子菜单悬停与键盘延迟:
- 级联子菜单在鼠标悬停时打开,并带有轻微的意图延迟(150–200ms 意图计时器),以防止指针移动期间意外触发闪烁。
- 在子菜单触发器上按下
ArrowRight可展开子菜单并将焦点移入其中;ArrowLeft可关闭子菜单并将焦点返回到父菜单项。
- 关闭触发器: 在以下情况下关闭打开的菜单覆盖层:
- 用户点击菜单面板外部(点击外部监听器)。
- 用户按下
Escape键。 - 执行了活动的菜单项操作(除非它是可勾选的切换项目)。
- 焦点离开菜单覆盖层。
4. 设计响应式移动适配
桌面下拉弹出框(例如 220px 浮动框)在移动触屏上表现不佳,因为点击目标小且定位要求精确:
- 移动触摸转换(
< 768px):- 将桌面弹出框转换为锚定在视口底部的 底部动作面板(
bottom-sheet-design-system)。 - 将项目行高增加到最小触控目标 44px(最好 48px),并采用全宽水平触控区域。
- 在面板底部提供明确的视觉拖拽手柄或清晰的“取消”按钮,以便轻松通过拇指关闭。
- 将桌面弹出框转换为锚定在视口底部的 底部动作面板(
- 触摸反馈: 应用不同的活动触摸背景状态(
:active)以获得即时触觉反馈。
5. 编程无障碍与键盘语义
- ARIA 地标与角色映射:
- 设置触发器按钮:
aria-haspopup="menu"、aria-expanded="true|false"、aria-controls="menu-id"。 - 设置菜单容器:
role="menu"、aria-labelledby="trigger-id"、tabindex="-1"。 - 设置菜单项:
role="menuitem"、role="menuitemcheckbox"或role="menuitemradio"。
- 设置触发器按钮:
- 键盘导航模型:
- 触发器上的
Enter/Space/ArrowDown:打开菜单并将焦点放在第一个菜单项上。 ArrowDown/ArrowUp:依次导航焦点通过菜单项(从顶部到底部环绕)。Home/End:将焦点直接移动到第一个或最后一个菜单项。Escape:立即关闭菜单并将焦点返回到触发器按钮。Tab:立即关闭菜单并允许标准文档焦点前进。- 键入时字符搜索: 按下字母键(例如“D”)可将焦点直接移动到以该字母开头的下一个菜单项。
- 触发器上的
决策规则
菜单类型与上下文选择
| 上下文 / 目标 | 推荐模式 | ARIA 结构 | 移动适配 |
|---|---|---|---|
| 行/卡片操作 | 操作下拉菜单 | role="menu" / role="menuitem" | 底部动作面板 |
| 工具栏溢出("...") | 溢出菜单 | role="menu" / role="menuitem" | 底部动作面板 |
| 右键画布工具 | 上下文菜单 | role="menu" / role="menuitem" | 长按底部面板 |
| 表单选项选择 | 组合框选择 | role="combobox" / role="option" | 原生选择 / 底部面板 |
| 多层视图/过滤器切换 | 可勾选操作面板 | role="menu" / role="menuitemcheckbox" | 底部动作面板 |
定位与对齐规则
- 数据表格行菜单: 相对于触发器按钮进行右上或右下对齐,以防止超出右侧表格视口边缘的水平截断。
- 标题/导航操作: 根据与屏幕边缘的接近程度,进行右下或左下对齐。
- 子菜单: 默认向右打开(
left: 100%);如果右侧空间小于 200px,则翻转向左打开(right: 100%)。
约束
- 无障碍(WCAG 2.1 AA):
- SC 1.4.3 对比度(最低): 项目文本、图标和键盘提示必须与菜单背景保持至少 4.5:1 的对比度。
- SC 2.1.1 键盘: 菜单内的每个项目及操作必须完全可通过键盘访问和触发。
- SC 2.4.7 焦点可见: 聚焦的菜单项必须具有独特的视觉焦点状态(例如背景色变化 + 符合对比度的突出指示符)。
- SC 2.5.8 目标尺寸: 桌面菜单项必须保持至少 32px 高度;移动动作面板项目必须满足至少 44x44px 的触控目标。
- 视口边界约束: 打开的菜单绝不能扩展到屏幕外或创建水平页面滚动条。使用碰撞翻转和内部垂直滚动(
max-height)。 - 空间内边距与密度: 保持一致的内部水平内边距(例如
padding: 0.5rem 0.75rem)和垂直容器内边距(padding: 0.25rem 0)。
常见失败模式
- 表单组合框混淆: 将
role="menu"用于表单选择下拉菜单,而不是role="combobox"/role="listbox",使屏幕阅读器混淆值选择与命令执行。 - 关闭时焦点迷失: 当菜单通过
Escape或选择关闭时,未能将键盘焦点恢复到触发器按钮,导致焦点丢失在document.body上。 - 裁剪容器陷阱: 将下拉菜单放置在带有
overflow: hidden的表格单元格或容器中,导致打开的菜单被视觉裁剪。 - 微小的移动点击目标: 在触摸屏上渲染微小的 24px 桌面下拉项目,而未将其转换为触控友好的底部面板。
- 意外子菜单关闭: 缺乏级联子菜单的意图计时器延迟,导致鼠标对角线移向子项目时子菜单折叠。
验证标准
- 菜单容器使用适当的 ARIA 语义(
role="menu"、role="menuitem"、role="separator")。 - 键盘导航完全支持
ArrowDown、ArrowUp、Home、End、Escape和字母键入搜索。 - 关闭菜单(通过
Escape或项目点击)后,程序化地将焦点恢复到触发元素。 - 视口碰撞规则可干净地翻转或滚动长菜单,而不会导致屏幕溢出。
- 响应式断点(
< 768px)将桌面下拉弹出框转换为触控友好的底部动作面板。 - 项目文本、快捷键提示和焦点指示符背景满足 WCAG AA 对比度(≥4.5:1)。