应使用语义化 <input type=”checkbox”> 实现通知开关,配 <label> 绑定,用 checked+onChange 做受控组件,视觉隐藏时保留可访问性,扩展点击区域并同步 aria-live 提示。

怎么用 <input type="checkbox"> 表达通知开关
通知开关本质是二元状态(开 / 关),<input type="checkbox"> 是语义最准确、无障碍支持最完善的选择。别用 <button> 或 <div> 模拟,那会破坏屏幕阅读器识别、键盘焦点流和原生表单提交行为。
常见错误现象:aria-checked="true" 配合 <div> 手动切换——看似能读,但无法用空格键触发、不参与 form 序列化、按回车无响应。
- 必须配
<label>,且用for属性或包裹方式绑定,否则点击文字不触发开关 - 禁用
role="switch":它仅适用于视觉上像物理开关的控件,且需额外处理aria-checked和键盘事件,纯增加复杂度 - 状态同步靠
checked属性,不是 class 名或自定义 data 属性
checked 和 defaultChecked 到底该用哪个
React 场景下容易混淆。服务端渲染或初始化即确定状态时,用 defaultChecked;运行时受 state 控制、需响应式更新时,必须用 checked + onChange。
错误示例:<input type="checkbox" defaultChecked={isNotifyOn} /> —— 这会导致 props 变化时 UI 不更新,因为 defaultChecked 只在挂载时生效。
立即学习 “ 前端免费学习笔记(深入)”;
- 受控组件:用
checked={isNotifyOn}+onChange={(e) => setIsNotifyOn(e.target.checked)} - 非受控组件(极少需要):用
defaultChecked,后续靠 ref 操作 DOM - SSR 同构应用中,
defaultChecked的值必须与服务端首次渲染一致,否则 React 会警告“prop mismatch”
为什么不能只靠 CSS 画个开关就完事
纯 CSS 实现的“滑块开关”(比如用 ::before/::after 伪元素盖住原生 checkbox)本身没问题,但很多人漏掉关键链路:隐藏原生控件时用了 display: none 或 visibility: hidden,这会让屏幕阅读器完全忽略它。
正确做法是用视觉隐藏技术,保留可访问性:
- 用
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;隐藏原生<input> - 确保自定义滑块区域有
tabindex="0"和完整键盘支持(空格键切换、Enter 键触发) - 如果用了
role="switch",必须同步设置aria-checked,且每次切换后手动更新
移动端点击区域太小导致误操作
原生 <input type="checkbox"> 默认点击热区极小(约 12×12px),在手机上极易点空。这不是样式问题,而是 HTML 规范里明确允许 label 扩展可点击区域。
- 把整个开关容器包进
<label>,而不是只包文字 - 或者给
<input>设置width/height并用transform: scale()放大,但注意别影响布局流 - 避免用
touch-action: manipulation粗暴提响应速度——它可能屏蔽长按等必要手势 - 测试真机:iOS Safari 对
label包裹svg或flex子项的支持不稳定,建议用块级容器
最常被忽略的是:开关状态变更后,没同步更新 aria-live 区域,导致屏幕阅读器用户不知道已生效。哪怕只是加一句 <div aria-live="polite" class="sr-only"> 通知已开启 </div>,体验就差很多。