tabindex="0"仅解决可聚焦,不解决交互、焦点样式和导航问题;必须为无原生聚焦能力但需键盘操作的容器添加,禁用于原生可聚焦元素、装饰性元素及子菜单项。

直接加 tabindex="0" 不等于键盘可访问——它只解决“能被 Tab 到”,不解决“按了没反应”“焦点看不见”“一按就跳页”这些真实问题。
哪些元素必须加 tabindex="0",哪些绝对不能加
只给业务上必须被键盘访问、又没有原生聚焦能力的容器加 tabindex="0",比如自定义下拉触发器、手写 tab 标题栏、折叠面板开关。这类元素通常是 div 或 h3,本身不可聚焦,但用户需要靠键盘操作它。
以下情况绝不能加:
-
button、a[href]、input等原生可聚焦元素——加了会干扰 React/Vue 的焦点同步或 SSR hydration - 纯图标
span、装饰性div、仅用于视觉分隔的容器——键盘用户停在那里既不能操作,也没语义 - 子菜单项(如下拉列表里的
li)——应由父容器用aria-activedescendant管理,而非每个都加tabindex="0"
tabindex="0" 加完后为什么按空格/回车没反应
因为键盘用户不“点击”,他们按 Enter 或 Space 触发行为。只监听 click 事件,等于对键盘用户关上了门。
必须同时做三件事:
- 监听
keydown(不是keyup),用event.key === 'Enter'或event.key === ' '判断(注意空格是字符,不是字符串'Space') - 按
Space时调e.preventDefault(),否则页面会向下滚动一屏 - 同步设置
role,比如role="button",否则屏幕阅读器读不出“这是个按钮”,只报“div”
焦点样式看不见,或者鼠标点一下也亮边框
清掉 outline 又不补替代方案,等于让键盘用户彻底失焦;全用 :focus 则鼠标用户每次点击都看到突兀轮廓,体验割裂。
正确做法是只在键盘触发聚焦时显示轮廓:
- CSS 中必须用
:focus-visible,例如:div[role="button"]:focus-visible { outline: 2px solid #007bff; } - 不要写全局
* { outline: none }——这是无障碍红线 - 如果用了
tabindex="-1"做程序化聚焦(如模态框打开后.focus()),也要确保该元素有:focus-visible样式,否则焦点移过去也看不见
方向键导航卡死、Tab 键跳出组件、嵌套菜单失焦
方向键(ArrowUp/ArrowDown)默认无逻辑;Tab 键离开组件后不拦截,焦点就掉到页面其他地方;嵌套组件没标记层级,外层会吃掉内层合法焦点转移。
关键动作要落在容器上:
- 监听容器的
keydown,用Array.from(items).indexOf(document.activeElement)算当前索引,再手动.focus()到目标项 - Home/End 键必须单独处理:跳首项或末项,别指望 Tab 循环自动覆盖
- 焦点移出组件时,靠
container.contains(document.activeElement)判断归属,再event.preventDefault()+items[0].focus()拉回来 - 嵌套组件(如菜单里套子菜单)要加
data-focus-lock标记,否则外层拦截会吃掉内层焦点转移
最常被忽略的一点:所有带 tabindex="0" 的自定义交互元素,背后都必须有一套完整的键盘行为闭环——从进得来(tabindex)、认得出(role)、按得动(keydown)、看得见(focus-visible),缺一不可。少一个环节,键盘用户就在那里卡住不动了。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











