aria-rowindex必须动态计算全局行号而非写死,因虚拟滚动复用DOM节点导致视觉位置与属性值错位;React中需基于原始数据索引(如start+index+1)更新,同时同步tabindex以保障键盘导航正确性。

aria-rowindex 不是“标当前行”的装饰属性,它是告诉屏幕阅读器“这个单元格在逻辑表格中属于第几行”的必要坐标。在虚拟长表格里,它必须和视觉位置严格一致,否则读屏会报错行号、跳行甚至卡死。
为什么直接写死 aria-rowindex 在虚拟滚动里会出错
虚拟滚动靠复用 DOM 节点来节省内存,比如只渲染可视区 + 缓冲区的 20 行,但数据总共有 10 万行。如果给每个 div 写死 aria-rowindex="5",滚动后这个节点可能已挪到第 9876 行,而属性没更新——屏幕阅读器就会坚持说“这是第 5 行”,和眼睛看到的完全对不上。
- 常见错误现象:NVDA 报“第 1 行”,实际焦点停在第 124 行数据上;JAWS 连续播报同一行号多次
- 根本原因:DOM 复用后,
aria-rowindex没随数据索引重算 - 必须配合:每次渲染该节点时,动态计算其在完整数据中的逻辑行号(从 1 开始),再赋值
React 中怎么安全更新 aria-rowindex
不能靠 index(渲染序号),得用数据本身的全局位置。假设你用 useVirtualScroll 计算出当前渲染的是 data.slice(start, end):
const Row = ({ index, data }) => {
// index 是 slice 后的局部索引
const globalRowIndex = start + index + 1; // +1 因为 aria-rowindex 从 1 起
return (
<div role="row">
<div role="gridcell" aria-rowindex={globalRowIndex} aria-colindex="1">
{data[index].name}
</div>
</div>
);
};
- 关键点:
globalRowIndex必须基于原始数据总索引,不是map的回调index - 别用
key值代替:key 可能是 id 或 hash,不是行号 - 如果用了
react-window,它的index参数就是 slice 后序号,需手动加偏移
容易被忽略的 tabindex 和 focus 同步问题
只设 aria-rowindex 不等于这行能被键盘导航选中。真实可用的网格还得让行获得焦点:
立即学习“前端免费学习笔记(深入)”;
- 可交互行(如支持点击/空格选中)要加
tabindex="0",且aria-rowindex必须存在 - 仅用于阅读的行,设
tabindex="-1",方便 JS 程序聚焦,但 Tab 键不跳过去 - 滚动后若某行刚进入可视区,又恰好是焦点目标,必须立刻更新它的
aria-rowindex和tabindex,否则键盘用户按 ↓ 键会跳到错误行 - 避免在
gridcell里嵌input时还设tabindex="0"——Tab 键会先进单元格,再进 input,重复聚焦
最麻烦的不是写对属性,而是滚动、排序、筛选、编辑后,所有 aria-rowindex、aria-colindex、tabindex 都得重新对齐真实数据位置。漏掉任意一个,AT 就会“迷路”。


















