手把手实现一个高性能的Switch开关组件

Good“钰浩 组件 阅读 1,902
赞 5 收藏
二维码
手机扫码查看
反馈

先说结论,我基本只用两种方案

做前端这么多年,Switch 开关这种组件看起来简单,但真要写得灵活、可维护、还能应对各种交互需求,其实挺容易踩坑的。我见过太多项目里一个 switch 组件写得五花八门:有人直接拿 input[type=checkbox] 改改样式就上线,有人非要用 JS 控制一切状态,还有人为了“高级感”引入一堆框架逻辑。

手把手实现一个高性能的Switch开关组件

我自己经历过几次重构,也接手过别人写的奇形怪状的开关,最后总结下来,真正能打的就两个方案:

  • 基于原生 checkbox + CSS 伪装(最常用)
  • 完全自定义 div + JS 控制(特殊场景才用)

React/Vue 那些封装都不算新方案,底层还是逃不开这两种思路。今天我就从实战角度聊聊它们的区别,以及我为啥大多数时候都选第一种。

谁更灵活?谁更省事?

先看第一个方案:原生 checkbox 伪装。这是我目前所有项目的默认选择,除非有明确理由不用,否则我都这么写。

<label class="switch">
  <input type="checkbox" class="switch-input" />
  <span class="switch-thumb"></span>
</label>
.switch {
  position: relative;
  display: inline-block;
  width: 50px;
  height: 30px;
  cursor: pointer;
}

.switch-input {
  opacity: 0;
  position: absolute;
  inset: 0;
  margin: 0;
  z-index: 1;
  /* 关键点:不能 display:none,否则 label 失效 */
}

.switch-thumb {
  position: absolute;
  inset: 0;
  background: #ddd;
  border-radius: 15px;
  transition: background 0.2s ease;
}

.switch-thumb::before {
  content: '';
  position: absolute;
  top: 4px;
  left: 4px;
  width: 22px;
  height: 22px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  transition: transform 0.2s ease;
}

.switch-input:checked + .switch-thumb {
  background: #16a34a;
}

.switch-input:checked + .switch-thumb::before {
  transform: translateX(20px);
}

这个方案的核心优势是:你根本不用管状态逻辑。checked 状态浏览器自动处理,点击 label 触发 input 切换也是原生行为。你要做的只是样式和结构。

而且它天生支持键盘访问(tab 进去回车切换),无障碍也基本没问题,screen reader 能读出 on/off 状态。这些细节要是自己手写 div 方案,分分钟漏掉。

那个“更高级”的全 JS 方案,其实坑不少

再来看看第二种:完全用 div 模拟,JS 控制状态。听起来好像更自由,但我现在是能避就避。

<div class="switch-custom" role="switch" aria-checked="false">
  <div class="switch-thumb-custom"></div>
</div>
const switchEl = document.querySelector('.switch-custom');
switchEl.addEventListener('click', () => {
  const isChecked = switchEl.getAttribute('aria-checked') === 'true';
  switchEl.setAttribute('aria-checked', !isChecked);
  // 还得手动同步 form 数据?麻烦死了
});
.switch-custom {
  position: relative;
  width: 50px;
  height: 30px;
  background: #ddd;
  border-radius: 15px;
  cursor: pointer;
}

.switch-thumb-custom {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 22px;
  height: 22px;
  background: white;
  border-radius: 50%;
  transition: transform 0.2s ease;
}

.switch-custom[aria-checked="true"] {
  background: #16a34a;
}

.switch-custom[aria-checked="true"] .switch-thumb-custom {
  transform: translateX(20px);
}

问题来了:

  • 你得自己管理状态,还得考虑是否要同步到表单数据
  • 键盘交互要额外绑定 keydown(比如空格键切换)
  • 无障碍需要手动设置 role 和 aria-checked,并监听变化
  • 如果嵌套在 form 里提交,还得搞 hidden input 同步值,不然拿不到数据

有一次我在一个后台系统里用了这种方案,结果测试报了 accessibility 问题,补了一堆 aria 和 keyboard 支持,折腾了半天发现还不如一开始就用 checkbox。那会儿真是边改边骂自己多此一举。

性能对比:差距比我想象的小

本来以为 JS 控制会更灵活,性能也更好,毕竟少了 input 元素。但实测下来,在现代浏览器里,这两种方案的渲染和响应速度几乎没差别。

我用 Chrome DevTools 跑了几轮交互 profiling,两者的主线程占用都在 1~2ms 左右,GC 也没明显波动。也就是说,你在这上面纠结性能,纯属浪费时间。真正影响体验的是交互流畅度和样式动画是否顺滑,而这更多取决于 CSS 动画实现方式,而不是底层用不用 input。

反倒是 JS 方案因为多了事件监听和属性操作,代码量更大,bundle size 更高一点。虽然这点差异可以忽略不计,但心理上总觉得“多做了事”,不如原生来的干净。

我的选型逻辑

我现在判断用哪个方案,就看三个问题:

  1. 要不要和其他表单字段一起提交? 要的话直接上 checkbox 方案,省心。
  2. 有没有复杂的交互逻辑? 比如双击触发不同行为、长按进入编辑模式之类的。这种时候 JS 控制反而更容易扩展。
  3. 设计师给了非常规动画? 比如滑块不是平移而是旋转出现,或者背景色渐变路径特别奇怪。这时候 CSS 可能搞不定,就得 JS 手动控制类名或 inline style。

90% 的情况答案都是第一个:要表单集成、不需要复杂交互、动画常规。所以我选 checkbox + CSS 伪装。

剩下 10% 是那种独立的状态控制器,比如“夜间模式”开关,根本不属于任何表单,点击还要发 API 请求更新用户偏好。这种我可能会用 JS 方案,方便接入事件回调。

举个例子,之前做过一个设置页,点击 switch 要调接口:

// 在 React 组件中
const handleToggle = async () => {
  const newValue = !isChecked;
  try {
    await fetch('https://jztheme.com/api/user/preference', {
      method: 'PATCH',
      body: JSON.stringify({ darkMode: newValue }),
      headers: { 'Content-Type': 'application/json' }
    });
    setIsChecked(newValue);
  } catch (err) {
    // 失败时回滚状态
    setIsChecked(!newValue);
  }
};

这种情况下,UI 只是状态的反映,真实状态由后端决定。用原生表单机制反而累赘,不如完全交给 JS 控制。

踩坑提醒:这三点一定注意

不管是哪种方案,这几个坑我都踩过,记下来提醒自己也提醒你:

  • 别给 input 加 display: none —— 这样 label 就失效了!必须用 opacity: 0 + position: absolute 覆盖。
  • CSS 动画别用 left/right 平移 —— 用 transform: translateX(),性能更好,不会触发重排。
  • 移动端记得加 touch-action: manipulation —— 防止点击延迟,特别是在 fastclick 已经淘汰的今天。

还有一个小技巧:如果你想让 switch 更容易点击,可以把 label 的 padding 扩大,视觉上不变,但点击区域更大。用户体验提升明显,尤其是手指粗的用户(笑)。

以上是我的对比总结,有不同看法欢迎评论区交流

说实话,没有哪个方案是完美的。checkbox 方案虽然省事,但定制样式有时候会被浏览器默认样式干扰,特别是老版本 iOS Safari 对 appearance 支持不好。JS 方案灵活,但容易写出不可维护的命令式代码。

我的建议是:先用简单的,不够用了再升级。别一上来就搞复杂抽象,很多项目根本不需要。

这个技巧的拓展用法还有很多,后续会继续分享这类组件实现细节。如果你有更好的实践,比如怎么优雅地做 loading 状态反馈,或者如何实现带文字标签的复合 switch,欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论