必须用 role="tablist" 构建语义化选项卡,配合 aria-controls/aria-labelledby 双向绑定、tabindex 管理、hidden 属性与 aria-selected 同步控制显隐,并实现箭头键导航及响应式角色切换。

用 role="tablist" 搭建语义化结构是必须的
只靠 div 堆砌或纯 CSS(比如 :checked)实现的选项卡,对屏幕阅读器和键盘用户基本不可用。浏览器不会识别“看起来像 tab”的元素为可交互控件,Tab 键无法进入,ArrowRight 也切不了页签。
正确起点是:外层容器加 role="tablist",每个页签用 <button role="tab"></button>,对应内容区用 <div role="tabpanel"></div>。三者必须通过属性双向绑定:
-
button上设aria-controls="panel-id",指向面板 ID -
tabpanel上设aria-labelledby="tab-id",反向指向按钮 ID - 所有
tab按钮需有tabindex="0",当前激活项改tabindex="-1"
切换时同步控制 aria-selected 和 hidden 属性
仅靠 CSS 的 display: none 或 visibility: hidden 隐藏非激活面板,屏幕阅读器仍可能朗读内容,且会干扰焦点管理。真正有效的隐藏方式是操作 hidden 属性 + aria-selected 状态。
- 激活的
tab按钮设aria-selected="true",其余为"false" - 对应
tabpanel移除hidden属性;其他全部加上hidden - 不要手动写
style.display = "none"—— 容易和外部样式冲突,尤其含position: absolute子元素时尺寸计算异常 - 在 CSS 中统一定义
[hidden] { display: none; },不加!important
键盘导航不能只靠默认 tabindex 顺序
用户按 Tab 进入 tablist 后,期望用左右箭头切换页签、Enter 激活、Home/End 跳首尾。这无法靠自然 DOM 顺序实现,必须监听 keydown 并手动管理焦点。
立即学习“前端免费学习笔记(深入)”;
- 给
tablist容器设tabindex="0",确保可聚焦 - 监听
ArrowLeft/ArrowRight,用Array.prototype.indexOf()找当前焦点索引,计算下一个并调用.focus() - 按
Home时聚焦第一个,End时聚焦最后一个 - 必须调用
e.preventDefault(),否则ArrowRight会触发页面横向滚动 - 垂直排列 Tab 时,显式加
aria-orientation="vertical",否则屏幕阅读器仍按水平逻辑播报
框架中别手动查 DOM 切 hidden,优先用条件渲染
在 React/Vue 里硬写 document.getElementById("panel-2").hidden = false,既破坏响应式数据流,又让组件状态和 DOM 不一致,测试困难,且容易漏掉 aria-hidden、tabindex 等同步更新。
- React 推荐用
useState管理当前 active key,配合{activeKey === "user" && <userpanel></userpanel>} - Vue 推荐用
v-if或v-show(后者保留 DOM,适合表单场景) - 避免用
v-if销毁含表单、定时器、动画的组件——输入内容会清空,倒计时会重置 - 若需保持状态但隐藏视觉,用
aria-hidden="true"+tabindex="-1"+ CSS 控制显隐更稳妥
最常被跳过的点是响应式断点下的角色退化:小屏时横向 Tab 折叠成下拉菜单,role="tablist" 就不再适用,得动态切为 role="combobox" 或 role="menu"。这个逻辑一旦漏掉,移动端键盘用户就彻底卡死。



















