原生不能做多级菜单,因其仅支持扁平选项且键盘行为硬编码,无法处理父子层级;必须用role="tree"/"treeitem"配合aria-expanded、aria-haspopup等实现可访问树形结构。

为什么原生 <select></select> 不能直接做多级菜单
因为 <select></select> 只支持扁平选项,浏览器对它的键盘行为是硬编码的:上下键切换、Enter 确认、Esc 关闭——没有 left/right 键处理父子层级的能力。强行用它嵌套子菜单,会破坏 role="tree" 的语义结构,导致 aria-expanded、aria-level 完全失效,屏幕阅读器读不出“可展开”或“第2层”这类信息。
role="tree" 和 role="treeitem" 必须成对出现
单独给某个 <li> 加 role="treeitem" 没有意义。必须确保最外层容器(通常是 <ul></ul>)有 role="tree",否则辅助技术根本不识别这是树形结构。
- 每个可展开节点(如“产品”)要同时带:
aria-expanded="false"(初始值不能省)、aria-haspopup="true" - 该节点内部必须包含一个
<ul role="tree"></ul>作为子树容器——漏掉这个role="tree"是高频错误,子项会被读作普通列表项,而非可操作节点 - 子树里的每个
<li>才能合法加role="treeitem"
方向键逻辑不是简单监听 ArrowDown 就完事
真实交互要求区分“同级移动”和“层级跳转”,且焦点不能落到隐藏内容上:
-
ArrowDown/ArrowUp:只在当前层级的可见treeitem间循环;不能让焦点跳进display: none的子菜单里 -
ArrowRight:若当前项已折叠,触发展开并立刻聚焦第一个子项;若已展开,则聚焦第一个子项(不是父项本身) -
ArrowLeft:若当前项是子项,收起其父节点并聚焦父项;若已是顶层项,忽略或聚焦父容器 - 隐藏子菜单必须用
aria-hidden="true"+tabindex="-1"控制焦点流,而不是仅靠 CSS 隐藏
焦点管理比视觉动画难得多
很多人花时间调 transform 和 z-index,却在焦点上栽跟头:
- 用户按 Tab 进入菜单后,焦点必须落在第一个
treeitem,不是触发按钮 - 展开子菜单时,焦点必须立刻移到子树第一个项,不能卡在父项上
- 按
Escape关闭整个菜单后,焦点应回到触发按钮(需triggerButton.focus()) - 移动端 touch 事件触发后,必须手动
.focus()对应项,否则键盘用户接不上后续操作
这些不是“优化项”,而是 WCAG 2.1 中 2.1.1 Keyboard 和 2.4.3 Focus Order 的强制要求。漏掉任意一条,等于把部分用户挡在门外。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











