必须用 ,因其原生支持 checked 状态、表单提交、屏幕阅读器识别、键盘切换及 change 事件;用 或 模拟会破坏可访问性、表单功能与行为一致性。

为什么必须用 <input type="checkbox"> 而不是 <div> 或 <code><button></button>
因为只有 <input type="checkbox"> 天然携带 checked 状态,能被表单自动提交、被屏幕阅读器识别、被键盘(空格键)切换。用 <div> 模拟,等于自己重写整套状态管理逻辑——但你根本没法保证它在语音控制、焦点导航、form submit 时行为一致。<p>常见错误是看到“开关好看”,就用 JS 绑定 click 改 class,结果:<code>form 提交时收不到值、Tab 键跳不过去、NVDA 读出来是“按钮”,而不是“启用通知,已关闭”。
-
<input type="checkbox">是唯一能原生响应change事件的元素,且该事件只在真实状态变化后触发 - 别给它加
display: none—— 这会让它彻底退出可访问性树;改用opacity: 0; position: absolute; clip: rect(0 0 0 0); -
id和for必须严格一致:大小写、连字符、空格都不能错,否则点击<label></label>不生效
label 怎么包才不掉链子
最稳写法是直接包裹:<label><input type="checkbox"></label>。这样不用管 id/for 匹配问题,点击 label 任意位置都触发 input,语义和兼容性全在线。
如果非要用分离结构(比如为了 CSS 定位方便),那就必须确保:<input id="notify"> 和 <label for="notify"></label> 的字符串完全相同——复制粘贴最安全,手敲容易漏掉字母或大小写。
- 别把
<input>放在<label></label>外面又没设for,那 label 就只是个普通文字容器 - label 必须有明确尺寸(比如
min-width: 44px)或display: inline-block,否则伪元素定位会漂移 - 加
cursor: pointer,让用户一眼看出这是可点的
CSS 动画写在哪、怎么动才不卡
动画不能写在 <input> 上——它不渲染任何东西。所有视觉变化必须落在 label::after 或额外的伪元素上,且只对 transform 和 background-color 做 transition。
滑块位移用 transform: translateX(),不是 left。前者走 GPU 加速,后者触发布局重排,手指一划就顿。
- 未选中时
label::after的transform: translateX(0);选中后设为translateX(calc(100% - 26px))(假设滑块宽 26px),这样能适配不同宽度轨道 - 过渡时间别超过
0.2s,0.3s开始有延迟感 - 轨道背景色别用纯黑/纯白,推荐
#4CAF50(选中)和#ccc(未选中),对比度达标且柔和
JavaScript 监听状态变化的坑
必须用 change 事件,不是 click。用户用空格键、语音指令、辅助设备切换时,click 根本不触发;而连续点两次已勾选的开关,click 会执行两次,change 只执行一次——这对发 API、存 localStorage 至关重要。
获取当前状态,永远读 input.checked,别信 class 名或 data 属性。JS 手动改 input.checked = true 后,浏览器不会自动派发 change,得手动 input.dispatchEvent(new Event('change'))。
- 绑定写法统一用
input.addEventListener('change', handler),别用onclick属性 - 移动端别监听 touchstart/touchmove 模拟拖拽——开关不是滑块,原生 checkbox 不支持拖动,强行做反而破坏无障碍
- 深色模式下记得加
@media (prefers-color-scheme: dark)单独调背景和滑块色,否则夜间模式里开关糊成一片
实际最难的部分不是写样式,而是让 checked 属性始终和视觉、API、本地存储三者同步。很多人调完动画就以为完成了,结果用户切了开关,页面显示变了,但后端根本没收到请求——问题往往出在 JS 里漏了 change 监听,或者用了错误的状态读取方式。











