aria-rowindex在虚拟滚动中易错位,因其非自动计数器,必须严格匹配视觉行数据;复用dom时若未同步更新aria-rowindex和tabindex,会导致屏幕阅读器播报行号与实际内容严重不符。

aria-rowindex 在虚拟滚动中为什么总“错位”
因为 aria-rowindex 不是自动更新的计数器,它必须和视觉上用户看到的那行数据严格对齐。虚拟滚动复用 DOM 节点时,如果只改了内容却忘了同步 aria-rowindex 和 tabindex,屏幕阅读器就会报“第 5 行”,而眼睛看到的是实际第 872 行的数据——这种错位在长表格里几乎必然发生,且无法被用户察觉,只能靠测试员手动核对。
滑动过程中如何动态更新 aria-rowindex
关键不是“监听 scroll”,而是“在渲染每一行前算清它的逻辑行号”。虚拟滚动的渲染函数(比如 React 的 useMemo 或原生 JS 的 renderVisibleRows())必须做三件事:
- 根据当前
scrollTop和单行高度,算出可视区起始逻辑行号(从 1 开始,不是数组索引) - 对每个即将插入的
<div role="gridcell">,显式写入 <code>aria-rowindex="{computedRowIndex}" - 若该行是表头(
role="rowheader"),也要设aria-rowindex,且值必须与数据行连续(例如表头固定在第 1 行,首行数据就是aria-rowindex="2") - 滚动到底部时,最后一屏可能不满整行数,但
aria-rowindex仍需按真实逻辑序号递增,不能“从 1 重新开始” - 使用
display: contents或visibility: hidden隐藏某几行?必须同步移除对应节点,或加aria-hidden="true",否则读屏会把隐藏行也计入行号计数 - 动态插入/删除行后,所有后续行的
aria-rowindex必须批量重算——不能只改新增行,否则行列索引链就断了 - iOS Safari 中,
requestAnimationFrame回调里更新属性比直接在scroll事件里更可靠,避免因节流导致索引滞后一帧 -
data-row-index:纯自定义属性,AT 完全无视 -
grid-row:CSS 布局属性,无障碍层面无语义 - 省略
aria-rowindex只靠父容器aria-rowcount:读屏无法定位具体哪一行,只会说“网格共 1000 行”,不告诉你当前焦点在哪
容易被忽略的边界情况
这些地方不处理,aria-rowindex 就会失效:
为什么不能靠 CSS grid-row 或 data-* 属性替代
aria-rowindex 是唯一被主流屏幕阅读器(NVDA、JAWS、VoiceOver)识别为“网格行位置”的标准属性。其他方式全无效:
最常被跳过的动作,是在 scroll 后忘记调用 focus() 到新聚焦的单元格——aria-rowindex 再准,没焦点也没人读它。











