Spring Toggle
An on/off switch whose knob stretches as it travels, then settles with a spring.
Preview and code
index.html
<button class="st" type="button" role="switch" aria-checked="false" aria-label="Notifications"><span class="st__k"></span></button>
styles.css
.st{position:relative;width:64px;height:36px;padding:0;border:0;border-radius:999px;background:color-mix(in srgb,var(--ink) 18%,transparent);cursor:pointer;transition:background .3s}
.st[aria-checked=true]{background:var(--accent)}
.st:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.st__k{position:absolute;top:4px;left:4px;width:28px;height:28px;border-radius:999px;background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.25);transition:left .45s cubic-bezier(.3,1.5,.5,1),width .2s}
.st:active .st__k{width:34px}
.st[aria-checked=true] .st__k{left:32px}
.st[aria-checked=true]:active .st__k{left:26px}
@media (prefers-reduced-motion:reduce){.st__k{transition:none}}
script.js
const s=document.querySelector('.st');s.addEventListener('click',()=>s.setAttribute('aria-checked',String(s.getAttribute('aria-checked')!=='true')));
spring-toggle.tsx
"use client";
import { useState } from "react";
const css = `
.st{position:relative;width:64px;height:36px;padding:0;border:0;border-radius:999px;background:rgba(127,127,127,.3);cursor:pointer;transition:background .3s}
.st[aria-checked=true]{background:#1659f5}
.st:focus-visible{outline:2px solid #1659f5;outline-offset:3px}
.st__k{position:absolute;top:4px;left:4px;width:28px;height:28px;border-radius:999px;background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.25);transition:left .45s cubic-bezier(.3,1.5,.5,1),width .2s}
.st:active .st__k{width:34px}
.st[aria-checked=true] .st__k{left:32px}
.st[aria-checked=true]:active .st__k{left:26px}
@media (prefers-reduced-motion:reduce){.st__k{transition:none}}`;
export function SpringToggle({ label, checked, defaultChecked = false, onChange }: { label: string; checked?: boolean; defaultChecked?: boolean; onChange?: (on: boolean) => void }) {
const [inner, setInner] = useState(defaultChecked);
const on = checked ?? inner;
const flip = () => { setInner(!on); onChange?.(!on); };
return (
<>
<style>{css}</style>
<button type="button" role="switch" aria-checked={on} aria-label={label} className="st" onClick={flip}>
<span className="st__k" />
</button>
</>
);
}
export default SpringToggle;
Install
npx shadcn@latest add https://ownlyplans.com/r/bhaskar/spring-toggle.json
Adds the React files to components/ownlyplans/ in a project set up with shadcn and Tailwind CSS. Or copy them from the Code tab.
About
A real button with role=”switch” and aria-checked, so keyboards and screen readers work out of the box.