自定义组件需设tabindex="0"以支持tab聚焦和键盘操作,配合keydown监听enter/space键并手动触发行为,使用:focus-visible控制焦点样式,同时结合role、aria-*等属性确保可访问性。

自定义组件默认无法被 Tab 键聚焦、不响应键盘事件、按空格/回车没反应——这不是 JS 写得不对,而是 HTML 语义和焦点管理没配对。
tabindex="0" 是让自定义元素进入键盘导航流的唯一合理方式
原生 <button></button>、<input> 天然可聚焦;但 <div>、<code><span></span> 或 Web Component 封装的按钮,必须显式声明可访问性。
-
tabindex="0":元素按 DOM 顺序加入 Tab 流,支持键盘聚焦 + 空格/回车触发(需手动监听) -
tabindex="-1":仅支持 JS 主动.focus(),比如模态框打开后聚焦标题,但用户无法用 Tab 到达它 - 避免正数
tabindex(如tabindex="1"):会打乱自然焦点顺序,导致可访问性工具误判,WCAG 明确不推荐
键盘事件监听必须覆盖 keydown + keyup + input 三类场景
只监听 click 或只绑 input 事件,会漏掉关键交互路径。例如:用户用空格激活一个 tabindex="0" 的卡片,或用方向键在菜单中移动焦点。
-
keydown:用于捕获功能键(Enter、Space、ArrowUp),适合触发动作,且能调用event.preventDefault()阻止默认行为 -
keyup:适合检测组合键释放(如Ctrl+S),但注意移动端无可靠keyup -
input:仅适用于文本类组件(如自定义<textarea></textarea>替代品),它在值变化后触发,比change更及时
空格键和回车键的行为不能混用,必须按语义区分
很多自定义按钮点击时用 click 监听,却忘了空格键在键盘聚焦状态下也会触发 click——但这只对原生 <button></button> 自动生效;对 div[tabindex="0"],你得自己桥接。
- 聚焦后按
Enter:应等同于点击,触发主操作(如提交、跳转) - 聚焦后按
Space:应等同于按下再松开,常用于切换(如 checkbox、toggle)、或“预览”类操作 - 两者都需在
keydown中拦截并手动 dispatchclick或调用 handler,否则键盘用户完全无法操作
键盘焦点样式必须用 :focus-visible 而非 :focus
直接写 button:focus { outline: 2px solid blue } 会让鼠标点击后也出现轮廓,干扰视觉。现代浏览器已支持 :focus-visible,它只在键盘触发聚焦时生效。
- 优先使用
:focus-visible,降级 fallback 可加@supports not selector(:focus-visible) - 不要移除所有 outline(哪怕换成 border):无焦点指示 = 键盘用户彻底失焦,违反 WCAG 2.4.7
- 如果组件内部有多个可聚焦子元素(如自定义下拉菜单),需用 JS 管理
roving tabindex:只允许一个子项tabindex="0",其余为-1
最常被忽略的点是:键盘交互不是“加几个事件监听器”就完事,而是要和 tabindex、role、aria-* 属性协同工作。比如一个自定义开关,除了 tabindex="0" 和 keydown,还必须带 role="switch" 和 aria-checked,否则屏幕阅读器根本不知道它是什么、当前状态如何。











