单选按钮组无障碍核心在于HTML结构:name必须完全一致,每个radio需唯一id并用for显式关联label,多维网格须用table+aria-labelledby,checked须在初始HTML中声明,隐藏时禁用display:none。

单选按钮组的无障碍访问体验,核心不在 JS 控制或 CSS 美化,而在 HTML 结构是否让屏幕阅读器能准确说出“你正在回答哪道题、当前选中的是哪个选项”。错一个 name、漏一个 id、少一个 aria-labelledby,用户就可能听到“radio button not checked”然后卡住。
必须用相同 name 且拼写完全一致
浏览器靠 name 属性原生管理互斥逻辑,不是靠 JS。大小写、空格、连字符不一致,等于创建多个独立组。
-
name="q1"和name="Q1"是两组,用户可同时选中两个 - 循环渲染时避免硬编码
name="option",应动态生成如name="question-{{id}}" - 用
document.querySelectorAll('input[type="radio"][name="theme"]')快速验证是否全部命中——返回数量必须等于你预期的选项数
每个单选按钮都要有唯一 id + 显式 for 关联
包裹式 <label><input>文本</label> 在移动端容易因 DOM 变动断开语义绑定,iOS VoiceOver 尤其敏感。显式 for/id 是唯一可靠方式。
-
for="q1-opt-a"必须字节级匹配id="q1-opt-a",尾部空格、大小写、下划线位置都算不一致 - 一组里不能共用一个
id,否则document.getElementById()只返回第一个,点击第 3 个 label 会聚焦到第 1 个 input - SSR 或 React/Vue 渲染时,
id必须带唯一标识(如id="q2-ans-0"),禁用框架默认 id(如 Vue 的v-for不自动提供 id)
多行多列网格必须用 <table> + <code>aria-labelledby
CSS Grid 或 Flex 布局只解决视觉对齐,无法表达“第 2 行 = Question 2,第 4 列 = +1 分”这种二维语义。屏幕阅读器只会读出孤立的 radio,不知道它属于哪道题、代表什么分值。
立即学习“前端免费学习笔记(深入)”;
- 每行用
<th scope="row" id="q2">Question 2</th>标记问题 - 每列用
<th scope="col" id="col-plus1">+1</th>标记选项 - 每个
<input type="radio">必须同时关联两者:aria-labelledby="q2 col-plus1" - 不要用
aria-label覆盖整段文字(如aria-label="Question 2, +1"),它会屏蔽原生 label 文本,破坏上下文可变性
checked 属性必须出现在初始 HTML 中
用 JS 执行 el.checked = true 或 el.setAttribute('checked', '') 都无法让屏幕阅读器识别默认选中项,也不会触发 aria-checked="true" 同步。
- 服务端渲染或静态 HTML:直接写
<input type="radio" name="theme" value="dark" checked> - React:用
defaultChecked,别用checked={true}(否则变成受控组件,切断无障碍链路) - Vue:用
v-model绑定初始值,而非在input上写:checked="true" - 动态切换时,应手动移除同组其他项的
checked属性(removeAttribute('checked')),而不是只设当前项
最易被忽略的一点:当单选按钮被 display: none 或 visibility: hidden 隐藏时,即使结构正确,它仍会进入可访问性树并干扰焦点流。真正安全的隐藏方式是 position: absolute; clip-path: inset(100%) —— 元素仍在 DOM 中、语义完整、键盘可聚焦,但视觉不可见。



















