Lever Switch
A chunky lever on a metal plate. Click it, or drag the knob, and the lever swings through the middle, looming towards you as it points straight out, then slams into place with a clunk: it bounces off the stop, the plate jolts and the little light beside “ON” comes on or goes out.
How to use it
- Paste the snippet just before
</head>. - Add
data-lever-switchto an empty button, with anaria-label. - Listen for its
changeevent;event.detail.onsays which way it went.
Options
Add any of these attributes to the same element.
data-on | Start switched on (lever up). |
data-color | Colour of the knob. Default #ef4444. |
data-light | Colour of the light when on. Default #22c55e. |
data-autoplay | It flips itself every couple of seconds while in view, until someone touches it, without firing the event. Handy for demos. |
Size it with font-size: it is 4em wide and 6em tall. It is a switch for screen readers, so Enter and Space flip it too. With reduced motion the lever just jumps.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Lever Switch: a chunky lever on a metal plate. Click it, or drag the
// knob, and the lever swings through the middle (looming towards you as
// it points straight out) and slams into place with a clunk: it bounces
// off the stop, the plate jolts and the light beside "ON" comes on or
// goes out.
// Usage: <button data-lever-switch aria-label="Power"></button>
// Optional: data-on (start switched on), data-color (colour of the knob,
// default #ef4444), data-light (colour of the light when on, default
// #22c55e), data-autoplay (it flips itself every couple of seconds while
// in view, without firing the event, until someone touches it; handy
// for demos).
// Events: fires "change" on the switch with event.detail.on.
// Size it with font-size: it is 4em wide and 6em tall.
var DEFAULTS = { color: '#ef4444', light: '#22c55e', every: 2200 };
var PX = 50, PY = 75, R = 36;
var uid = 0;
var css =
':where(.lvr){font-size:inherit}' +
'.lvr{position:relative;display:inline-block;width:4em;height:6em;padding:0;border:0;background:none;cursor:pointer;' +
'touch-action:none;-webkit-tap-highlight-color:transparent;vertical-align:middle}' +
'.lvr:focus-visible{outline:3px solid #f59e0b;outline-offset:4px;border-radius:.6em}' +
'.lvr svg{display:block;width:100%;height:100%;overflow:visible;pointer-events:none;filter:drop-shadow(0 .25em .4em rgba(15,15,30,.25))}' +
'.lvr-txt{font:800 9px/1 system-ui,sans-serif;letter-spacing:.08em;fill:#71717a;text-anchor:middle}' +
'.lvr-led{transition:fill .15s}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function build(id, color) {
return '<svg viewBox="0 0 100 150" aria-hidden="true"><defs>' +
'<linearGradient id="' + id + '-plate" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f4f4f5"/><stop offset="1" stop-color="#c4c4cc"/></linearGradient>' +
'<linearGradient id="' + id + '-arm" gradientUnits="userSpaceOnUse" x1="44" y1="0" x2="56" y2="0"><stop offset="0" stop-color="#52525b"/><stop offset=".4" stop-color="#d4d4d8"/><stop offset="1" stop-color="#3f3f46"/></linearGradient>' +
'<radialGradient id="' + id + '-knob" cx=".35" cy=".3" r=".75"><stop offset="0" stop-color="#fff" stop-opacity=".85"/><stop offset=".25" stop-color="' + color + '"/><stop offset="1" stop-color="' + color + '"/></radialGradient>' +
'</defs>' +
'<g class="lvr-body">' +
'<rect x="8" y="4" width="84" height="142" rx="14" fill="url(#' + id + '-plate)" stroke="#a1a1aa" stroke-width="1"/>' +
'<circle cx="18" cy="14" r="2.6" fill="#a1a1aa"/><circle cx="82" cy="14" r="2.6" fill="#a1a1aa"/>' +
'<circle cx="18" cy="136" r="2.6" fill="#a1a1aa"/><circle cx="82" cy="136" r="2.6" fill="#a1a1aa"/>' +
'<text class="lvr-txt" x="50" y="22">ON</text><text class="lvr-txt" x="50" y="137">OFF</text>' +
'<circle class="lvr-led" cx="72" cy="19" r="3.6" fill="#d4d4d8" stroke="#a1a1aa" stroke-width=".8"/>' +
'<rect x="41" y="31" width="18" height="88" rx="9" fill="#27272a"/>' +
'<rect x="43" y="33" width="14" height="84" rx="7" fill="#18181b"/>' +
'<ellipse class="lvr-shadow" cx="50" cy="75" rx="13" ry="9" fill="#000" opacity=".25"/>' +
'<line class="lvr-arm" x1="50" y1="75" x2="50" y2="40" stroke="url(#' + id + '-arm)" stroke-width="9" stroke-linecap="round"/>' +
'<circle class="lvr-knob" cx="50" cy="40" r="13" fill="url(#' + id + '-knob)" stroke="rgba(0,0,0,.2)" stroke-width="1"/>' +
'</g></svg>';
}
function setup(el) {
var d = el.dataset;
var id = 'lvr-' + (++uid);
el.classList.add('lvr');
el.setAttribute('role', 'switch');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Power');
el.innerHTML = build(id, d.color || DEFAULTS.color);
var body = el.querySelector('.lvr-body');
var arm = el.querySelector('.lvr-arm');
var knob = el.querySelector('.lvr-knob');
var shadow = el.querySelector('.lvr-shadow');
var led = el.querySelector('.lvr-led');
var light = d.light || DEFAULTS.light;
var on = d.on !== undefined && d.on !== 'false';
// t runs from -1 (lever up, on) to 1 (lever down, off).
var st = { t: on ? -1 : 1 };
function draw() {
var t = st.t, near = 1 - Math.min(1, Math.abs(t));
var y = PY + t * R;
arm.setAttribute('y2', y);
arm.setAttribute('stroke-width', 9 + near * 2);
knob.setAttribute('cy', y);
knob.setAttribute('r', 13 * (1 + 0.28 * near));
shadow.setAttribute('cy', y + 6 + near * 4);
shadow.setAttribute('cx', 50 + 3 + near * 3);
shadow.setAttribute('opacity', 0.18 + near * 0.14);
}
function paint() {
el.setAttribute('aria-checked', on ? 'true' : 'false');
led.setAttribute('fill', on ? light : '#d4d4d8');
led.style.filter = on ? 'drop-shadow(0 0 3px ' + light + ')' : '';
}
// Swing to the end, slam into the stop and bounce off it.
function swing(fire) {
var end = on ? -1 : 1;
anime.utils.remove(st);
if (reduced()) { st.t = end; draw(); paint(); return; }
var dist = Math.abs(end - st.t);
anime.animate(st, {
t: end, duration: 120 + 140 * dist / 2, ease: 'in(3)', onUpdate: draw,
onComplete: function () {
paint();
anime.animate(st, { t: [end - end * 0.16, end], duration: 420, ease: 'outElastic(1, .4)', onUpdate: draw });
anime.animate(body, { y: [0, end * -2.2, end * 1, 0], duration: 220, ease: 'out(2)' });
anime.animate(el, { scale: [1, 0.985, 1], duration: 200, ease: 'out(2)' });
}
});
if (fire) el.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { on: on } }));
}
function toggle(fire) {
on = !on;
swing(fire);
}
draw();
paint();
var userTouched = false, drag = null, skipClick = false;
el.addEventListener('pointerdown', function (e) {
if (e.button !== 0) return;
userTouched = true;
var r = el.getBoundingClientRect();
drag = { y: e.clientY, t: st.t, scale: 150 / r.height, moved: false };
anime.utils.remove(st);
try { el.setPointerCapture(e.pointerId); } catch (err) {}
});
el.addEventListener('pointermove', function (e) {
if (!drag) return;
var dy = (e.clientY - drag.y) * drag.scale;
if (Math.abs(dy) > 3) drag.moved = true;
if (!drag.moved) return;
st.t = Math.max(-1, Math.min(1, drag.t + dy / R));
draw();
});
function end() {
if (!drag) return;
var moved = drag.moved;
drag = null;
if (!moved) { skipClick = false; return; }
skipClick = true;
var want = st.t < 0;
if (want !== on) toggle(true);
else swing(false);
}
el.addEventListener('pointerup', end);
el.addEventListener('pointercancel', end);
el.addEventListener('click', function (e) {
if (skipClick) { skipClick = false; return; }
if (e.isTrusted) userTouched = true;
toggle(true);
});
if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
var timer = null;
var loop = function () {
if (userTouched) return;
toggle(false);
timer = setTimeout(loop, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(loop, 800);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-lever-switch]').forEach(function (el) {
if (el._lvr) return;
el._lvr = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button id="power" data-lever-switch aria-label="Power"></button>
<script>
document.getElementById("power").addEventListener("change", (e) => {
console.log(e.detail.on ? "On" : "Off");
});
</script>