树结构必须用语义化html与完整aria属性实现:外层role="tree",节点role="treeitem",子列表用;需动态同步aria-expanded、aria-level、aria-setsize/posinset;键盘导航须支持方向键、enter/space等闭环操作;懒加载需aria-busy与aria-live配合。

树结构必须用- 而非嵌套
语义错误是可访问性崩塌的第一步。很多团队用一堆
<div> 模拟树节点,结果屏幕阅读器完全读不出层级、无法识别“可展开”或“已选中”。<code><ul></ul> 和 <li> 天然表达父子/兄弟关系,是 WAI-ARIA 树规范的底层基础。
- 必须把最外层容器设为
role="tree",每个节点用 role="treeitem"
- 子节点列表要用
<ul></ul> 包裹,并加 role="group",不能用 <div> 或 <code><span></span> - 不要手动写
tabindex="0" 给每个 <li> —— 这会破坏键盘导航流,应只让当前焦点项有 tabindex="0",其余设为 -1
aria-expanded 和 aria-level 缺一不可
光有角色不够,状态和位置信息缺失会让辅助技术用户迷失在“哪一层?展开没?还有几个兄弟?”的困惑里。
-
aria-expanded 必须动态同步:点击折叠时设为 false,DOM 中对应子 <ul></ul> 也要隐藏(display: none 或 hidden)
-
aria-level 要从 1 开始逐层递增,根节点是 1,子节点是 2,孙子是 3 —— 不能靠 CSS 类名或 data 属性模拟
-
aria-setsize 和 aria-posinset 要在渲染时计算并写死:比如第 3 个同级节点,就设 aria-posinset="3",该层级共 5 个,就设 aria-setsize="5"
键盘导航逻辑不能只靠 onclick
仅监听 click 事件,等于默认放弃键盘用户。方向键、Enter、Space 的行为必须由 JS 显式接管,且焦点管理要闭环。
- 上下方向键:在所有可见节点间循环切换焦点(不是全部节点),跳过已折叠的子树
- 左右方向键:左键收起当前展开节点;右键展开当前折叠节点(若含子项)
- Enter / Space:激活当前焦点项(如选中、触发回调),不是单纯 toggle 展开
- Home / End:跳转到第一个/最后一个可见节点,不是 DOM 中第一个/最后一个
<li>
懒加载节点必须带 aria-busy 和 aria-live
异步加载子节点时,如果没提示,屏幕阅读器会静默卡住,用户不知道“是不是卡了”还是“还没加载完”。
- 开始请求前,给当前节点加
aria-busy="true",移除 aria-expanded
- 插入新子节点后,立刻移除
aria-busy,恢复 aria-expanded="true"
- 用
<div aria-live="polite"> 容器包裹加载状态文本(如“正在加载子节点…”),确保语音播报<li>新插入的子节点必须补全所有 ARIA 属性(<code>role="treeitem"、aria-level、aria-setsize 等),不能只塞 HTML 片段
复杂点不在写多少代码,而在每一步都得同时满足 DOM 结构、ARIA 状态、键盘行为、焦点流四者严格对齐——少一个,对依赖屏幕阅读器的用户来说,整棵树就变成不可操作的黑箱。
<div> 模拟树节点,结果屏幕阅读器完全读不出层级、无法识别“可展开”或“已选中”。<code><ul></ul> 和 <li> 天然表达父子/兄弟关系,是 WAI-ARIA 树规范的底层基础。
- 必须把最外层容器设为
role="tree",每个节点用role="treeitem" - 子节点列表要用
<ul></ul>包裹,并加role="group",不能用<div> 或 <code><span></span> - 不要手动写
tabindex="0"给每个<li>—— 这会破坏键盘导航流,应只让当前焦点项有tabindex="0",其余设为-1 -
aria-expanded必须动态同步:点击折叠时设为false,DOM 中对应子<ul></ul>也要隐藏(display: none或hidden) -
aria-level要从 1 开始逐层递增,根节点是 1,子节点是 2,孙子是 3 —— 不能靠 CSS 类名或 data 属性模拟 -
aria-setsize和aria-posinset要在渲染时计算并写死:比如第 3 个同级节点,就设aria-posinset="3",该层级共 5 个,就设aria-setsize="5" - 上下方向键:在所有可见节点间循环切换焦点(不是全部节点),跳过已折叠的子树
- 左右方向键:左键收起当前展开节点;右键展开当前折叠节点(若含子项)
- Enter / Space:激活当前焦点项(如选中、触发回调),不是单纯 toggle 展开
- Home / End:跳转到第一个/最后一个可见节点,不是 DOM 中第一个/最后一个
<li> - 开始请求前,给当前节点加
aria-busy="true",移除aria-expanded - 插入新子节点后,立刻移除
aria-busy,恢复aria-expanded="true" - 用
<div aria-live="polite"> 容器包裹加载状态文本(如“正在加载子节点…”),确保语音播报<li>新插入的子节点必须补全所有 ARIA 属性(<code>role="treeitem"、aria-level、aria-setsize等),不能只塞 HTML 片段
aria-expanded 和 aria-level 缺一不可
光有角色不够,状态和位置信息缺失会让辅助技术用户迷失在“哪一层?展开没?还有几个兄弟?”的困惑里。键盘导航逻辑不能只靠 onclick
仅监听 click 事件,等于默认放弃键盘用户。方向键、Enter、Space 的行为必须由 JS 显式接管,且焦点管理要闭环。懒加载节点必须带 aria-busy 和 aria-live
异步加载子节点时,如果没提示,屏幕阅读器会静默卡住,用户不知道“是不是卡了”还是“还没加载完”。复杂点不在写多少代码,而在每一步都得同时满足 DOM 结构、ARIA 状态、键盘行为、焦点流四者严格对齐——少一个,对依赖屏幕阅读器的用户来说,整棵树就变成不可操作的黑箱。











