纯前端实现可访问数据表格:焦点管理、排序播报与固定列
前端表格很容易做成“看起来像表格”的 div 组合:排序能点、横向能滚,但读屏用户不知道当前按哪列排序,键盘用户也很难在大量单元格中定位。可靠的数据表格要从语义、状态和焦点三条线一起设计。
一、优先使用原生表格结构
静态数据优先使用 table、thead、tbody、th 和 td。表头加 scope="col",行首标识加 scope="row",浏览器和辅助技术就能建立行列关系。不要为了圆角或阴影把整张表拆成互不相关的 div。
<table>
<thead>
<tr>
<th scope="col" aria-sort="ascending">
<button type="button">名称</button>
</th>
</tr>
</thead>
<tbody>...</tbody>
</table>
只有当单元格需要二维光标移动、编辑或复杂选择行为时,才考虑 role="grid"。选择 grid 就意味着要完整实现方向键、Home、End、编辑和退出规则,不能只借它的名字。
二、把排序状态放进数据模型
排序不能只改图标。维护列标识和方向两个值,并把列的 aria-sort 设置为 ascending、descending 或 none。点击表头后同步更新数据、图标和可访问名称:
const sort = { key: 'updatedAt', direction: 'descending' };
function toggleSort(key) {
const direction = sort.key === key && sort.direction === 'ascending'
? 'descending'
: 'ascending';
sort = { key, direction };
renderRows();
announce('已按' + labelOf(key) + (direction === 'ascending' ? '升序' : '降序') + '排列');
}
原始数组不要原地排序;复制后再排,能让重置排序和后续缓存更简单。比较日期、数字和文本时使用不同类型规则,不要全部依赖字符串默认顺序。
三、用 live region 播报结果
视觉上的箭头变化不会自动被读屏用户感知。准备一个视觉隐藏、aria-live="polite" 的状态区,在筛选、排序或分页完成后写入简短结果,例如“共 32 条,按更新时间降序”。连续快速操作时可以用 requestAnimationFrame 或短防抖合并播报,避免信息刷屏。
四、区分浏览焦点和操作焦点
大多数原生表格不需要给每个单元格加 tabindex。排序和筛选控件本身应处于正常 Tab 顺序,行内操作使用明确的按钮。如果业务确实需要二维键盘导航,可用一个可聚焦表格容器记录 activeCell,再用方向键移动,同时保证按 Tab 能离开表格。
固定表头或首列时,焦点轮廓可能被 overflow 裁切。给交互元素留足内边距,并用 box-shadow 或内描边保证焦点可见;不要全局删除 outline。
五、固定列和横向滚动
首列可以用 position: sticky 固定在左侧,但需要明确的 background、z-index 和右侧阴影,否则滚动时会和其他列混在一起。表头单元格要同时处理顶部和左侧固定,避免出现双层覆盖。滚动容器本身应可通过键盘聚焦,并提供“横向滚动区域”等可读标签。
六、移动端不要强行缩成桌面表
窄屏下可以转成每条记录一张信息卡,也可以只保留主标识列并提供详情入口。关键字段不能因为列太多而悄悄消失;如果使用横向滚动,要在视觉之外明确提示还有更多列。
七、测试重点
至少验证纯键盘操作、读屏软件中的表头关系、排序方向变化、筛选后结果数量播报、固定列滚动以及空数据和错误状态。表格的可访问性不是加一个 aria-label 就结束,它需要语义、状态和操作路径保持一致。
八、小结
优先原生表格,给表头建立明确作用域,把排序方向写成 aria-sort,用 live region 播报结果,键盘焦点保持在真实控件上。固定列解决的是阅读连续性,移动端降级解决的是信息完整性,两者都应服从同一套数据语义。
- 点赞
- 收藏
- 关注作者
评论(0)