手把手打造高性能Toggle切换器的实战技巧
我为什么又要重新看一遍Toggle?
说实话,写这个组件的时候我心里有点烦。又不是没写过Toggle,但每次项目一换技术栈,或者UI设计扔过来一个“稍微不一样”的动效需求,我就得重新翻一遍方案。React、Vue、原生自定义元素、Tailwind……每种都有人说好,但我踩过的坑告诉我:没有银弹。
这次是给一个管理后台做暗黑模式切换,需求看着简单:一个圆钮在轨道里滑动,开/关状态分明。结果设计稿上的过渡动画是缓入缓出+颜色渐变,开发时才发现默认的CSS transition根本不够用。折腾了两天才定下来用哪个方案,所以干脆记一笔,下次别再重复造轮子了。
谁更灵活?谁更省事?
我对比了三个主流做法:
- 纯CSS + label hack(最老派)
- React + state控制的div模拟(现在最多人用)
- Web Components封装(最近开始用,真香)
先说结论:日常项目我选React那个;如果组件要复用到多个项目,或者团队有统一组件库计划,我会上Web Components。至于纯CSS?只适合静态页面或者Demo,真上生产环境迟早出事。
CSS Label Hack:轻量但脆弱
这招我大学时候就学会了——用隐藏的checkbox + CSS伪类来模拟Toggle。确实代码少,也不依赖框架。
<label class="toggle">
<input type="checkbox" class="toggle-input" />
<span class="toggle-slider"></span>
</label>
.toggle-input {
opacity: 0;
position: absolute;
}
.toggle-slider {
display: inline-block;
width: 50px;
height: 24px;
background: #ccc;
border-radius: 12px;
position: relative;
cursor: pointer;
transition: background 0.3s;
}
.toggle-slider::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: transform 0.3s;
}
.toggle-input:checked + .toggle-slider {
background: #4caf50;
}
.toggle-input:checked + .toggle-slider::before {
transform: translateX(26px);
}
看起来很完美对吧?但问题来了:一旦你想加一个“加载中”状态,或者禁用点击时保留视觉反馈,你就得改DOM结构,甚至引入JS。而且accessibility支持得自己补全aria-*属性,不然残障用户直接懵逼。
我之前在一个政府项目里用这方案,验收时被无障碍检测工具打了回来,补了一堆aria-checked和role="switch",最后还不如一开始就用JS控制。
React状态驱动:自由度最高
我现在大部分项目都用React,所以更倾向用state来控件状态。虽然代码多几行,但逻辑清晰,扩展性强。
import { useState } from 'react';
function Toggle({ onChange, defaultChecked = false }) {
const [checked, setChecked] = useState(defaultChecked);
const handleClick = () => {
const newValue = !checked;
setChecked(newValue);
onChange?.(newValue);
};
return (
<div
className={toggle ${checked ? 'toggle-on' : 'toggle-off'}}
onClick={handleClick}
role="switch"
aria-checked={checked}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
<div className="toggle-handle"></div>
</div>
);
}
.toggle {
width: 50px;
height: 24px;
background: #ddd;
border-radius: 12px;
position: relative;
cursor: pointer;
transition: background 0.3s ease;
}
.toggle-on {
background: #4caf50;
}
.toggle-handle {
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.toggle-on .toggle-handle {
transform: translateX(26px);
}
这个方案的好处是你可以在onChange里塞任何逻辑:发请求、打埋点、控制全局主题切换。而且动画曲线也能自定义,比如我上面用了cubic-bezier,滑动更有弹性感。
坑也不是没有:如果你不在意性能,可能会忘记防抖。曾经有一次用户连点五次,结果触发了五次API调用,后端报警了。后来我在onChange外面包了一层防抖函数才解决。
Web Components:一次封装,到处跑
最近公司搞微前端,各个团队技术栈不一样,React/Vue/Angular都有,这时候我就开始尝试用Web Components写基础组件。
class CustomToggle extends HTMLElement {
constructor() {
super();
this.checked = this.hasAttribute('checked');
this.disabled = this.hasAttribute('disabled');
}
connectedCallback() {
this.render();
this.addEventListener('click', this.toggle.bind(this));
}
toggle() {
if (this.disabled) return;
this.checked = !this.checked;
this.setAttribute('checked', this.checked);
this.render();
const event = new CustomEvent('change', {
detail: { checked: this.checked },
bubbles: true,
});
this.dispatchEvent(event);
}
render() {
this.innerHTML =
<style>
:host {
display: inline-block;
width: 50px;
height: 24px;
background: ${this.checked ? '#4caf50' : '#ccc'};
border-radius: 12px;
position: relative;
cursor: ${this.disabled ? 'not-allowed' : 'pointer'};
transition: background 0.3s;
}
.handle {
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
position: absolute;
top: 2px;
left: ${this.checked ? '26px' : '2px'};
transition: left 0.3s;
}
</style>
<div class="handle"></div>
;
}
}
customElements.define('custom-toggle', CustomToggle);
注册之后 anywhere 都能用:
<custom-toggle checked></custom-toggle>
<script>
document.querySelector('custom-toggle').addEventListener('change', (e) => {
console.log(e.detail.checked);
// fetch('https://jztheme.com/api/theme', {
// method: 'POST',
// body: JSON.stringify({ darkMode: e.detail.checked })
// });
});
</script>
这个方案最大的优点是解耦。React项目可以直接当HTML标签用,不需要引入额外依赖。而且样式隔离做得好,不怕全局污染。
缺点也很明显:调试麻烦,Chrome DevTools里看不到props,只能靠attributes。还有就是IE11不支持,如果你还得兼容老浏览器,那就别玩这个了。
我的选型逻辑
我现在是怎么选的?很简单:
- 内部管理系统,React为主 → 用React组件
- 多团队协作、跨框架 → 上Web Components
- Demo或静态页 → 直接CSS hack凑合
还有一个细节很多人忽略:**动画完成后再执行回调**。比如你要切主题,最好是动画滑完再改CSS变量,不然会有闪烁。我现在的做法是在React版本里用requestAnimationFrame延迟dispatch,效果顺滑很多。
另外提一嘴Tailwind。如果你用Tailwind,其实可以直接用class切换,但我不推荐把逻辑塞进className里。还是那句话:可读性比少写两行代码重要得多。
以上是我的对比总结,有不同看法欢迎评论区交流
这三个方案我都在线上项目用过,各有各的适用场景。没有绝对的好坏,只有适不适合当前项目节奏和团队能力。
我个人现在更偏向于用React + TypeScript封装一个通用Toggle,加上onBeforeChange钩子用来拦截异步操作(比如保存失败就不允许切换),这样既灵活又可控。
Web Components虽然前景好,但周边生态还是弱,比如测试工具链、文档生成都不如React成熟。等哪天它像Button一样普及了,我可能才会全面切换。
总之,别迷信“最先进”,选你能维护得住的方案才是正道。毕竟我们是写业务的,不是炫技大赛选手。

暂无评论