纯CSS radio方案无法满足WCAG 2.1可访问性要求,因其存在键盘不可聚焦、DOM结构脆弱、ARIA状态无法同步三类硬伤;合规方案必须用JS+ARIA实现tablist结构、属性控制、焦点管理和URL同步。

纯 CSS radio 方案做选项卡,表面能动,实际在可访问性上基本不可用——键盘切不进去、屏幕阅读器读不准、刷新后状态丢失,不是“够用”,而是“不能上线”。真要合规,必须用 JS + ARIA 控制结构。
为什么 input[type="radio"] + :checked 无法满足 WCAG 2.1
它看起来简单,但一放到真实场景里就暴露三类硬伤:
-
tabindex缺失:input默认不可键盘聚焦(除非显式加tabindex="0"),label又没role="tab",Tab 键根本进不去标签栏 - DOM 依赖太脆弱:
:checked ~ .panel或相邻选择器要求 radio 和面板严格相邻或同级;加个divwrapper、用 JS 动态插入内容,样式就断掉 -
aria-selected和aria-hidden完全靠 CSS 模拟,JS 不参与状态管理,URL hash 同步、页面刷新回溯、屏幕阅读器 announce 都做不到
role="tablist" 结构必须手动初始化
ARIA 规范要求首次加载时,必须显式设置初始选中态,否则辅助技术无法感知“当前在哪”。这不是可选动作,是强制前提:
- 给第一个
button[role="tab"]加aria-selected="true"和tabindex="0" - 对应面板(
div[role="tabpanel"])加aria-hidden="false",且确保有id与按钮的aria-controls匹配 - 所有未激活 tab 必须设
tabindex="-1"(不是tabindex="0"),否则键盘会错误地停在非活动项上 - 别漏掉外层容器:
div[role="tablist"]要带aria-label或aria-labelledby,告诉用户这是什么功能区
切换逻辑只改属性,不动 style.display
用 JS 直接操作 element.style.display = "none" 是反模式。它会覆盖 CSS 媒体查询、动画过渡,还会让 iframe/video 暂停、表单值丢失。正确做法是只更新 ARIA 属性,交由 CSS 控制显隐:
立即学习“前端免费学习笔记(深入)”;
- 点击时,仅调用
tabButton.setAttribute("aria-selected", "true")和panel.setAttribute("aria-hidden", "false") - 用 CSS 写:
[role="tabpanel"][aria-hidden="true"] { display: none; },或更稳妥的visibility: hidden; position: absolute; - 动画要用
opacity+transform,避免触发布局重排;过渡必须加在面板元素上,而非靠 JS 插入 class 触发 - 别忘了同步 URL:
history.pushState({}, "", "#tab-2"),并在popstate里还原状态
最易被忽略的点:焦点管理不是“加上就行”,而是“每次切换后必须主动 focus() 到新面板的首个可聚焦子元素”。否则键盘用户会卡在空白区域,屏幕阅读器也不会自动朗读新内容。



















