单纯靠 tabindex 控制网格内 tab 导航路径注定失败,因其只决定能否被聚焦,不控制跳转逻辑、方向键行为或语义;正确方案需结合 role="grid"/"row"/"gridcell"、aria 关联及 tabindex="-1" 配合脚本聚焦。

单纯靠 tabindex 控制网格内 Tab 导航路径,在复杂布局中注定失败——它只决定“能不能被 Tab 键碰到”,不负责“怎么跳”“跳到哪”,更不管方向键或上下文语义。
为什么 grid 容器加 tabindex="0" 无法让键盘用户按行列走
很多人给 display: grid 的父容器加 tabindex="0",以为这样就能把整个网格变成一个可聚焦的“大单元格”,再靠方向键导航。实际效果是:焦点只停在容器上,内部所有子项(哪怕有 tabindex="0")全被跳过;或者焦点直接穿透进第一个可聚焦子元素,但后续 Tab 就乱序了。
-
tabindex不改变 DOM 顺序,Tab 键永远按 HTML 源码顺序流转,不是视觉网格顺序 - Grid 子项默认不在 Tab 流中(除非是原生可聚焦元素,如
button或带tabindex的div),仅靠 CSS 布局无法赋予语义 - 屏幕阅读器完全无视
grid-template-columns,只认role和aria-关联
role="grid" 是起点,不是装饰
要让屏幕阅读器识别“这是个二维表格式控件”,必须显式声明 role="grid",且立刻禁用其默认焦点行为:tabindex="-1"。否则,它会和内部可聚焦子项争夺焦点,导致键盘用户一 Tab 就卡住或跳失。
- 容器设
role="grid" tabindex="-1",表示“我是个语义网格,但别让我自己收焦点” - 每一行必须是
role="row",不能用div或section替代;每单元格必须是role="gridcell",不能混用article或li - 若某单元格含按钮/链接,需额外加
aria-labelledby或aria-describedby把上下文(如行标题、列标题)关联进去,否则操作时读不出“第3行‘满意度’列的‘非常满意’按钮”
网格内单选按钮组必须用 <table> + <code>aria-labelledby
用 CSS Grid 或 Flex 实现的评分量表(如 Likert 量表),对键盘用户和读屏软件来说就是一堆散落的 input[type="radio"],毫无行列归属。唯一可靠方案是回归语义化 <table> 结构,并用 <code>aria-labelledby 双向绑定。
- 每行用
<th scope="row" id="q1">问题1</th>标明行头 - 每列用
<th scope="col" id="col-plus2">+2</th>标明列头 - 每个单选按钮写
aria-labelledby="q1 col-plus2",确保播报为“问题1,+2” - 绝对不要用
role="grid"包裹一堆input——它无法替代<table> 的行列语义,且不支持 <code>scope属性编辑器类动态网格中,
tabindex="-1"比tabindex="0"更关键在代码编辑器、低代码平台这类含折叠面板、工具栏、实时预览区的复合网格中,
tabindex="0"只能用于极少数明确需要手动聚焦的入口点(如可展开的属性面板标题)。真正驱动导航的是tabindex="-1"配合脚本聚焦。- 面板展开后,焦点必须立即移到首个输入框,该输入框需提前设
tabindex="-1",否则.focus()失效 - 模态弹窗打开时,第一个可操作元素(如确认按钮)也得设
tabindex="-1",再调用.focus() - 禁止给
button、input等原生可聚焦元素加tabindex="0"——它会干扰 React/Vue 的焦点管理,且在 SSR 场景下引发 hydration mismatch - 移动端 Safari 会忽略大部分
tabindex="0",但tabindex="-1"+.focus()在所有平台都稳定生效
最常被忽略的一点:网格是否“可导航”,不取决于你写了多少
tabindex,而取决于你有没有为每个交互点提供明确的role、上下文aria-关联、以及聚焦后的可视反馈(:focus-visible)和滚动对齐(scrollIntoView)。没有这三者,tabindex再多也只是摆设。 - 面板展开后,焦点必须立即移到首个输入框,该输入框需提前设
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











