Toggle Switch
An on/off switch whose knob springs across, stretching as it goes and wobbling to a stop, while the track floods with colour. It’s a real switch, with role="switch" and aria-checked.
How to use it
- Paste the snippet just before
</head>. - Add
data-toggle-switchto a button. Any text inside becomes its label.
Options
Add any of these attributes to the same element.
data-on | Start switched on. |
data-color | Track colour when on. Default #6a0dad. |
data-autoplay | It flips itself every couple of seconds while in view. Handy for demos. |
Size it with font-size. It fires a change event: e.detail.checked is true or false. From code, animacious.toggle(button) flips it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Toggle Switch: an on/off switch whose knob springs across, stretching as
// it goes and wobbling to a stop, while the track floods with colour. It's
// a real switch: role="switch" and aria-checked follow it, and Space or
// Enter flip it.
// Usage: <button data-toggle-switch>Dark mode</button>
// Any text inside becomes the switch's label, shown before it.
// Optional: data-on (start switched on), data-color (track colour when on,
// default #6a0dad), data-autoplay (it flips itself every couple of seconds
// while in view; handy for demos). Size it with font-size.
// Listen for the "change" event: e.detail.checked is true or false.
// Script use: animacious.toggle(button) flips it from code.
var DEFAULTS = { color: '#6a0dad', off: '#d4d0de', every: 2000 };
var TRAVEL = 1.1; // em the knob moves
var css =
'.tsw{display:inline-flex;align-items:center;gap:0.6em;padding:0;border:0;background:none;font:inherit;color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.tsw-track{position:relative;flex:none;width:2.7em;height:1.6em;border-radius:0.8em;background:var(--tsw-off)}' +
'.tsw-knob{position:absolute;left:0.2em;top:0.2em;width:1.2em;height:1.2em;border-radius:0.6em;background:#fff;box-shadow:0 0.08em 0.25em rgba(15,15,30,.3)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(btn) {
var d = btn.dataset;
btn.classList.add('tsw');
if (btn.tagName === 'BUTTON' && !btn.getAttribute('type')) btn.type = 'button';
btn.setAttribute('role', 'switch');
btn.style.setProperty('--tsw-off', DEFAULTS.off);
var label = btn.textContent.trim();
btn.textContent = '';
if (label) {
var text = document.createElement('span');
text.textContent = label;
btn.appendChild(text);
}
var track = document.createElement('span');
track.className = 'tsw-track';
track.setAttribute('aria-hidden', 'true');
var knob = document.createElement('span');
knob.className = 'tsw-knob';
track.appendChild(knob);
btn.appendChild(track);
var on = d.on !== undefined && d.on !== 'false';
btn._tsw = { track: track, knob: knob, on: on, color: d.color || DEFAULTS.color };
btn.setAttribute('aria-checked', on);
anime.utils.set(knob, { x: on ? TRAVEL + 'em' : '0em' });
anime.utils.set(track, { backgroundColor: on ? btn._tsw.color : DEFAULTS.off });
}
function toggle(btn) {
var s = btn._tsw;
var on = (s.on = !s.on);
btn.setAttribute('aria-checked', on);
btn.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { checked: on } }));
var to = { x: on ? TRAVEL + 'em' : '0em' };
var bg = on ? s.color : DEFAULTS.off;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
anime.utils.set(s.knob, to);
anime.utils.set(s.track, { backgroundColor: bg });
return;
}
// The knob stretches towards where it's going (leading edge first), then
// springs past and wobbles to a stop.
anime.utils.set(s.knob, { transformOrigin: on ? '100% 50%' : '0% 50%' });
anime.animate(s.knob, {
x: { to: to.x, duration: 650, ease: 'outElastic(1, .55)' },
scaleX: [{ to: 1.35, duration: 120, ease: 'out(2)' }, { to: 1, duration: 380, ease: 'outElastic(1, .5)' }],
scaleY: [{ to: 0.88, duration: 120, ease: 'out(2)' }, { to: 1, duration: 380, ease: 'outElastic(1, .5)' }]
});
anime.animate(s.track, { backgroundColor: bg, duration: 300, ease: 'out(2)' });
}
function init() {
document.querySelectorAll('[data-toggle-switch]').forEach(function (btn) {
if (btn._tsw) return;
setup(btn);
btn.addEventListener('click', function () { toggle(btn); });
if (btn.dataset.autoplay !== undefined && btn.dataset.autoplay !== 'false' &&
!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
var timer = null;
var tick = function () {
if (!btn.matches(':hover') && document.activeElement !== btn) toggle(btn);
timer = setTimeout(tick, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 600);
}).observe(btn);
}
});
}
window.animacious = window.animacious || {};
window.animacious.toggle = function (btn) { if (btn._tsw) toggle(btn); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button data-toggle-switch>Dark mode</button>
<!-- on to begin with, in green: -->
<button data-toggle-switch data-on data-color="#16a34a">Notifications</button>