键盘焦点导航需综合事件监听、焦点校验、语义标记与边界控制;tabindex="0"是基础但非万能,原生可聚焦元素默认支持,div/span等须显式添加;禁用正数tabindex、避免tabindex="-1"误用于容器;css隐藏或outline缺失会使tabindex失效;keydown中慎用preventdefault(),仅拦截目标键;模态框须实现焦点环;移动端需手动聚焦+滚动适配;焦点管理须协同aria-modal等语义属性。

键盘焦点导航不是“加个 tabindex 就能用”,而是必须配合事件监听、焦点校验、语义标记和边界控制,否则用户按 Tab 会跳过元素、卡死在输入框、或直接穿出模态框。
tabindex="0" 是进入 Tab 流的最低门槛,但不是万能开关
原生可聚焦元素(input、button、a[href]、select)默认就带 tabindex="0",不用重复加;但 div、span、自定义组件必须显式加 tabindex="0" 才能被 Tab 键访问到。
- 别用
tabindex="-1"给本该可导航的容器(比如下拉菜单根div),它会让整个区域对键盘“不可见” - 绝对避免
tabindex="1"或正数——DOM 顺序被打乱后,屏幕阅读器和键盘用户的操作路径会完全错位 - 检查 CSS:如果元素有
visibility: hidden、display: none或outline: none(没配:focus-visible),即使tabindex="0"也无效
keydown 中调用 preventDefault() 很容易切断导航链
很多开发者在 input 上监听 keydown 并无条件调用 event.preventDefault(),结果用户按 Tab 出不去、按 Enter 提交不了表单、按 Escape 关不掉弹窗。
- 只对目标键拦截:
if (e.key === 'Enter' && e.target.tagName === 'INPUT') { e.preventDefault(); doSearch(); } - 放行关键导航键:
if (['Tab', 'Enter', 'Escape', 'ArrowUp', 'ArrowDown'].includes(e.key)) return; - 值校验优先用
input事件,它不干涉焦点移动,比keydown更安全
模态框/弹窗里 Tab 键跳出边界,本质是没做焦点环(focus trap)
浏览器不会自动把 Tab 锁在弹窗里。只要没手动拦截并重定向,焦点就会一路走到 body 底部甚至外部 iframe。
- 打开时用
requestAnimationFrame聚焦首个可聚焦子元素:modal.querySelector('button, input, [tabindex="0"]') - 监听弹窗容器的
keydown,仅当e.key === 'Tab'且当前聚焦的是最后一个/第一个可聚焦项时才e.preventDefault() - 用
Array.from(modal.querySelectorAll(...))缓存焦点元素列表,避免每次重复查询 - 关闭前必须缓存触发源(如按钮的
id),关闭后先校验该元素是否仍在 DOM 中,再.focus()
移动端软键盘唤起后焦点丢失或滚动异常
iOS Safari 对 autofocus 支持极差,Android Chrome 可能将聚焦元素滚出视口顶部——这不是 bug,是系统级行为,必须主动适配。
- 不要依赖
autofocus属性,改用requestAnimationFrame(() => el.focus()) - 聚焦后手动滚动:调用
el.scrollIntoView({ block: 'nearest', inline: 'nearest' }) - 若弹窗含异步内容(如 fetch 后渲染表单),聚焦逻辑必须等真实 DOM 挂载完成,不能只靠
showModal()回调 - 测试时务必在真机上用软键盘操作,模拟器无法复现滚动截断问题
最常被忽略的一点:焦点管理不是“写完就跑”,而是要和 aria-modal、inert、aria-hidden 配合使用——否则屏幕阅读器会读出背景内容,键盘用户却无法操作它,形成语义与行为的割裂。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











