tabindex="-1"允许js程序化聚焦但排除在tab顺序外,适用于模态框首项或非语义元素;勿用于原生可聚焦元素,且需配合focus()及兼容性处理preventscroll。

tabindex="-1" 能让元素获得焦点但不进 tab 顺序
很多开发者以为 tabindex 只有 0 和正数两种用法,其实 tabindex="-1" 是键盘导航中控制焦点流最常用的“隐形开关”。它允许 JS 主动调用 focus() 把焦点给某个元素(比如模态框打开后自动聚焦到第一个输入框),又不会让这个元素被用户按 Tab 键意外跳入——避免打断正常阅读流。
常见错误是给按钮或链接加 tabindex="-1" 后发现键盘无法触发点击:这是因为语义化元素(<button></button>、<a href></a>)默认就在 tab 顺序里,加 -1 相当于主动踢出,除非你有明确理由(比如临时禁用某控件的键盘访问),否则不该这么做。
- 只对非交互语义元素(如
<div>、<code><span></span>)用tabindex="-1"实现程序化聚焦 - 模态框(modal)首次打开时,用
element.focus()聚焦到第一个可操作项,该元素需有tabindex="-1"或本身是可聚焦语义元素 - 别给
<input>、<select></select>等原生表单控件设tabindex="-1"——它们默认可聚焦,设了反而可能破坏辅助技术识别 - 如果只需确保焦点落在元素上,用基础
element.focus()即可 - 如果必须禁止滚动(例如侧边栏菜单展开后聚焦某项,但不想页面跳动),先检测支持性:
if ('preventScroll' in FocusOptions.prototype) { element.focus({preventScroll: true}); } else { element.focus(); } - 注意:即使加了
preventScroll,元素若本身不可见(display: none或visibility: hidden),焦点仍不会成功设置 - 标准做法:对可点击区域,同时监听
keydown(处理Enter)和keyup(处理Space),并在keydown中检查event.key === 'Enter',在keyup中检查event.key === ' '(注意是空格字符) - 更稳妥的方式是直接监听
click事件——它天然兼容鼠标、空格、回车,且不依赖按键时机 - 如果必须用键盘事件(比如要做快捷键组合),记得在
keydown中对Space调用preventDefault(),否则会触发页面滚动 - 容器必须有
role="listbox"或类似语义角色,否则aria-activedescendant不会被读屏软件识别 - 被指向的子项(如
<div role="option" id="item-2">)不能有 <code>tabindex,否则会混入真实 tab 顺序 - 键盘导航逻辑(上下键切换、Home/End 跳转)必须由 JS 完整实现,浏览器不代劳
focus() 和 focus({preventScroll: true}) 的实际差异
调用 focus() 默认会滚动页面使元素进入视口,这在长列表或固定头部布局里常导致界面“跳闪”。Chrome 86+ 支持 focus({preventScroll: true}),但 Safari 目前仍不支持该选项,直接传对象参数会静默失败(元素依然获得焦点,但没报错)。
所以真实项目里得做兼容处理:
键盘事件中 keydown vs keyup 判断 Enter/Space 的坑
监听表单提交或按钮点击时,很多人在 keydown 里拦截 Enter,却忘了 Space 键在原生按钮上是 keyup 才触发。这是浏览器行为差异:按钮(<button></button>)和复选框(<input type="checkbox">)对空格键的响应发生在 keyup,而 Enter 在 keydown 就已触发默认行为。
文章转信息图。将文章/笔记转化为手机可读的 HTML 信息图,自动匹配视觉风格。触发场景:文章转图、笔记转图、信息图、转小红书图、做张图、可视化这篇文章、文生图。
结果就是:只监听 keydown 并 preventDefault() 的自定义按钮,按空格没反应;只监听 keyup 的,则可能错过 Enter 的及时响应。
aria-activedescendant 配合 tabindex="0" 实现虚拟列表焦点管理
长列表(如下拉选择器、树形组件)如果每个选项都渲染成真实 DOM 并设 tabindex="0",会导致 tab 顺序过长、屏幕阅读器朗读混乱。正确做法是只让容器有 tabindex="0",再用 aria-activedescendant 指向当前高亮项的 id,由 JS 控制键盘方向键移动焦点“视觉状态”。
这个模式不是自动生效的:你得自己监听 ArrowDown/ArrowUp,更新 aria-activedescendant 值,并调用 element.setAttribute('aria-activedescendant', 'item-2'),同时确保目标元素有 id="item-2"。
焦点导航真正难的不是写对几个属性,而是判断什么时候该用真实聚焦、什么时候该用虚拟聚焦,以及如何让 JS 行为和浏览器默认行为不打架。多数可访问性问题,都出在“半截子实现”——比如加了 tabindex 却没配键盘事件,或者用了 aria-activedescendant 却忘了设 role。










