role="switch"必须用非表单元素实现,需显式设置aria-checked="true"/"false"、tabindex="0"、监听space/enter键,并用aria-labelledby关联标签,否则屏幕阅读器无法正确识别开关控件。

role="switch"不是给加的装饰,而是替代它的一套手动可访问性实现——用错就等于关掉屏幕阅读器的识别能力。
为什么不能直接写 <input type="checkbox" role="switch">
浏览器会忽略 role="switch",因为原生 <input type="checkbox"> 已有固定语义,ARIA role 在这里被覆盖或冲突。Safari + VoiceOver 下可能同时读出“复选框”和“开关”,造成双重播报;NVDA 则可能完全跳过该元素。真正起作用的只有原生 checked 属性,aria-checked 不会被监听。
- 必须用非表单元素实现,比如
<div> 或 <code><span></span> - 不能依赖隐藏原生 checkbox 后再套 role——那只是视觉欺骗,语义链已断裂
- 若你本就可以用原生
<input type="checkbox">,且不需要物理开关动效,就别碰role="switch" - 初始值不能省略:哪怕默认关闭,也得写
aria-checked="false",否则部分读屏报“未定义” - 禁止用
aria-checked="1"或aria-checked="0":必须是字符串"true"/"false" - 状态更新必须由 JS 主动触发:点击后改样式,也得同步执行
el.setAttribute('aria-checked', 'true') - 不要用 class 名(如
is-on)或自定义data-属性代替——辅助技术不读这些 - 必须加
tabindex="0",否则无法进入键盘操作流 - 必须监听
keydown,对Space键调用preventDefault()并翻转状态 -
Enter键建议也响应,保持与多数 UI 框架行为一致 - 焦点样式(
:focus-visible)不能仅靠 outline:需确保视觉反馈与aria-checked状态严格对应 - 优先用
aria-labelledby="id"关联附近可见<label></label>,例如:<label id="notify-label">通知提醒</label><div role="switch" aria-labelledby="notify-label" aria-checked="false"></div> - 避免
aria-label="开启通知"这类带动作动词的写法——开关描述的是状态或功能,不是指令 - 若 label 内容含 HTML 或动态文本,可用
aria-describedby补充说明,但不能替代主标签 - 不要把文字塞进 switch 元素内部(如
<div role="switch">Wi-Fi</div>):它不是文本容器,读屏可能跳过或误读
aria-checked 必须显式设置且实时同步
只写 role="switch",屏幕阅读器根本不会识别为开关控件,大概率当普通静态容器处理。它需要 aria-checked="true" 或 aria-checked="false" 才能激活语义。
键盘支持不是可选项,是强制闭环
没有 tabindex="0",用户无法用 Tab 键聚焦;没监听 Space 键,键盘用户就切不了状态;不处理 Enter,某些读屏(如旧版 JAWS)可能误判为按钮行为。
标签不可省,aria-labelledby 优于 aria-label
一个光秃秃的滑块 + role="switch",读屏只会说“开关”,用户不知道它管什么。这不是“加个提示”而已,而是语义结构的刚需。
最常被绕开的其实是「状态同步闭环」:每次视觉切换,都要确认三件事是否同时发生——DOM 上 aria-checked 值变了、JS 里控制状态的变量变了、焦点管理逻辑没漏掉。少一环,键盘或读屏用户就卡在某个状态里出不来。











