role="tree"在旧版talkback中无效,因其12以下版本不解析动态注入的aria-expanded或role="treeitem",仅读取初始dom静态语义;嵌套超4层或含冗余div容器会引发低配设备卡顿,须精简为nav>ul>li>a结构,子列表必须是li的直接子ul,收起时保留dom并仅用aria-hidden="true"屏蔽,各节点需唯一id并通过aria-labelledby显式关联上级标题,同时支持方向键导航与焦点自动管理。

为什么 role="tree" 在旧版 TalkBack 里根本没用
不是写上就生效。TalkBack 12 以下版本(覆盖 Android 8–10 大量存量设备)压根不解析动态注入的 aria-expanded 或 role="treeitem",只读取初始 DOM 中静态存在的语义。如果你 JS 动态插入节点并补 role,这些属性会被静默丢弃。
更麻烦的是:嵌套超过 4 层的 div + role 组合,在内存 ≤2GB 的设备上会触发渲染卡顿,焦点移动延迟常超 800ms。这不是体验差,是根本不可操作。
- 检查当前结构是否含
div>div>ul>li>div>a这类冗余嵌套——必须压到nav>ul>li>a - 禁用所有中间容器的
role,只在最外层ul写role="tree",每个li写role="treeitem" - 子列表必须是父
li的直接子ul,不能用div.tree-children包裹
收起状态必须保留 DOM,不能用 display: none
display: none 会从可访问性树中彻底移除节点,屏幕阅读器无法聚焦、键盘无法抵达、aria-hidden="true" 也失效。低配环境依赖“内容始终在线”,哪怕视觉上收起。
正确做法是让子 ul 始终在 DOM 中,仅靠 aria-hidden="true" 屏蔽,同时确保其父节点 button 的 aria-expanded 值与视觉状态严格同步。
- 初始 HTML 中所有子
ul默认可见(不加style="display:none") - JS 控制展开/收起时,只切换
aria-expanded和aria-hidden,不操作display - 收起时给子
ul加aria-hidden="true";展开时移除该属性 - 禁止对子
ul使用visibility: hidden或opacity: 0,它们不阻断焦点流
每个节点必须有唯一 id 并用 aria-labelledby 关联上级
只写“前端工程师”,屏幕阅读器不会告诉你它属于哪个团队。老版本 NVDA 和 TalkBack 不解析 aria-describedby 的上下文变化,但 aria-labelledby 支持多 ID 空格分隔,且播报顺序固定。
ID 必须真实存在、全局唯一。若引用的 ID 不存在,部分读屏会静默失败——既不报错,也不播报关联文本。
- 上级标题(如
h3)加id="team-web" - 节点
li内部用div aria-labelledby="team-web dev-senior-fe" - 多个 ID 按播报优先级排序,空格分隔,不要用逗号或其它符号
- 避免用
span作为可聚焦容器——老 TalkBack 无法聚焦span,必须用button或带tabindex="0"的div
键盘导航不能只靠 Tab 键
几十个节点全靠 Tab 跳转,用户得按几十次键。真实可用的树形菜单必须支持方向键:左/右控制展开收起,上/下线性遍历同级,Enter/Space 触发操作。
这要求每个可交互节点都有明确的 tabindex 状态管理,且 JS 必须监听 keydown 并阻止默认行为,否则方向键会滚动页面。
- 可展开节点用
button,天然支持 Space/Enter,无需额外绑定 - 非按钮类节点(如带复选框的
div)必须设tabindex="0",并在keydown中手动处理方向键 - 每次焦点移动后,立即调用
element.focus(),别依赖浏览器自动聚焦 - 展开子节点后,焦点应自动落到第一个子
treeitem,而非停留在父节点
aria-expanded 当成装饰属性,忘了它必须和视觉状态、DOM 存在性、焦点可达性四者完全同步。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











