aria-level 必须手动设置且与 role="tree" 结构严格对齐,不可依赖 dom 嵌套或 css/js 推导;每个 role="treeitem" 需显式设 ≥1 的正整数 aria-level,并配合 aria-posinset 和 aria-setsize 才构成完整树形语义。

aria-level 不是自动推导的,必须手动写死
浏览器和屏幕阅读器不会根据 DOM 嵌套深度自动计算 aria-level。哪怕你用三层 <ul><li><ul><li><ul><li></ul></li></ul></li></ul> 嵌套,不显式写 aria-level="3",读屏器就只当它是 level 1 —— 它只认属性值,不看结构。
常见错误是依赖 CSS 类名(如 menu-level-2)或 JS 自动递增逻辑,结果在动态插入节点、懒加载展开、服务端渲染与客户端 hydrate 不一致时,aria-level 漏设或错位,导致语音播报“总监 → 组长 → 实习生”全在同一层级,完全丢失管理关系。
- 每个
role="treeitem"节点都必须有aria-level,且值为正整数(从 1 开始) - 根级节点(如 CEO)固定为
aria-level="1",其直系下属为"2",依此类推 - 同一父节点下的所有子项
aria-level值必须相同;跨父节点的同级项也应保持数值一致(比如所有部门负责人都是 level 2) - 不要用
aria-level="0"或负数——规范明确要求 ≥1,NVDA 和 VoiceOver 会忽略非法值
aria-level 必须与 role="tree" 结构对齐
aria-level 只在树形上下文中有意义。单独给一个 <div role="treeitem"> 加 <code>aria-level="2",但外层没包 role="tree",这个属性会被完全忽略。它不是通用装饰属性,而是树形语义链的一环。
典型翻车场景:把整个导航栏用 <nav></nav> 包着,里面混用 <ul></ul> 和 <div role="treeitem">,却忘了最外层 <code><ul></ul> 要加 role="tree"。结果 NVDA 把所有节点读作“列表项”,跳过 aria-level,用户根本听不出层级。
- 必须确保:最外层容器有
role="tree"(通常是<ul></ul>或<div>) <li>每个可聚焦/可操作的节点用 <code>role="treeitem",并配aria-level - 子树容器(即下一级
<ul></ul>)也要显式声明role="tree",否则其内部的aria-level不被识别 - 别在
<table> 或 <code><select></select>里用aria-level——这些元素自带固定语义,强行注入会触发 ARIA 校验警告动态树里 aria-level 容易漏更新
懒加载或折叠展开时,新插入的子节点如果没同步写
aria-level,读屏器就会把它当作 level 1 插入到当前上下文中,造成“子公司经理”和“集团 CEO”平级播报。这不是视觉 bug,是可访问性断裂。React/Vue 场景下尤其危险:组件复用时,父组件传入的
depthprop 若未透传到子节点的aria-level,或者 key 复用导致旧 DOM 属性残留,都会让aria-level固定在初始值不动。- 插入新节点时,必须基于父节点的
aria-level+ 1 计算,不能硬编码 - 收起再展开时,要检查子树是否已存在
aria-level;若无,则补上(避免 SSR 渲染缺失) - 用 MutationObserver 监听新增的
role="treeitem"元素,自动打标aria-level是个兜底策略,但不如初始化时就写对 - 测试时打开 NVDA 的“元素浏览模式”,按 Insert+B 查看标题/树项层级,比靠眼睛看 DOM 更可靠
aria-level 和 aria-setsize/aria-posinset 配合才完整
只有
aria-level不够。屏幕阅读器用户想快速定位“技术部第 3 个岗位”,需要知道当前节点在整个兄弟集中的位置和总数。单靠aria-level="2",只能告诉用户“这是第二层”,但无法回答“这是第二层里的第几个”。常见疏忽是只设
aria-level,却漏掉aria-setsize和aria-posinset。结果用户听到“前端工程师”,不知道这是“Web 团队”的第一个还是最后一个,也无法用 Home/End 键跳转到同级首尾项。- 每个
role="treeitem"应同时设置:aria-posinset(当前序号)、aria-setsize(兄弟总数) - 这两个值必须严格对应真实 DOM 子节点数量,不能靠 class 数量或 JS 数组 length 估算(可能含隐藏项)
- 折叠状态下,子树仍保留在 DOM 中(仅
aria-hidden="true"),所以aria-setsize不变,但aria-posinset仍按展开后的完整顺序计算 - 不要用
aria-setsize="undefined"或空字符串——读屏器会跳过该属性,当成无序集合处理
aria-level,而是每次 DOM 变动后,你有没有重新校验整棵树的层级、序号、展开态三者是否同步。漏掉任意一环,对键盘和读屏用户来说,就是整棵树塌了一角。 - 插入新节点时,必须基于父节点的











