Accordion expandWeb · App
Target
Implement the "Toggle switch" effect in vanilla HTML/CSS/JS. Context: A switch whose thumb slides across with a small overshoot while the track colour changes. Trigger: the element is pressed (pointerdown → pointerup). Timeline: time-based (duration + easing). Motion tokens (normal): - translateX (thumb): 0 → track width − thumb width - background (track): neutral → accent - duration: 220ms - easing: cubic-bezier(0.34, 1.56, 0.64, 1) - note: track colour uses 150ms ease; thumb uses the overshoot curve Motion tokens (prefers-reduced-motion): - translateX (thumb): 0 → track width − thumb width - background (track): neutral → accent - duration: 120ms - easing: ease-out - note: same motion, no overshoot Technique: prefer pure CSS transitions/animations; use JS only to toggle classes. Acceptance: - animate only transform and opacity unless the spec lists other properties - no layout shift; content is visible without the animation - when prefers-reduced-motion is set, use the reduced tokens only - use a real button with role=switch and aria-checked; Space toggles it - thumb position comes from transform, not left/right
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-toggle-switch" data-fx-root> <button type="button" class="fx-switch" role="switch" aria-checked="false" aria-label="Notifications"><span class="fx-thumb"></span></button> </div>
demo.css
.fx-toggle-switch { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-toggle-switch .fx-switch { position: relative; width: 64px; height: 36px; padding: 0; border: 0; border-radius: 999px; background: var(--fx-strong, #d4d4d8); cursor: pointer; transition: background-color 150ms ease; }
.fx-toggle-switch .fx-thumb { position: absolute; top: 4px; left: 4px; width: 28px; height: 28px; border-radius: 50%; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.2); transition: transform 220ms cubic-bezier(.34, 1.56, .64, 1); }
.fx-toggle-switch .fx-switch[aria-checked="true"] { background: var(--fx-accent, #4f46e5); }
.fx-toggle-switch .fx-switch[aria-checked="true"] .fx-thumb { transform: translateX(28px); }
.fx-toggle-switch .fx-switch:focus-visible { outline: 2px solid var(--fx-accent, #4f46e5); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .fx-toggle-switch .fx-thumb { transition-duration: 120ms; transition-timing-function: ease-out; } }
demo.js
export default function mount(root) {
const sw = root.querySelector('.fx-switch');
const toggle = () => sw.setAttribute('aria-checked', sw.getAttribute('aria-checked') === 'true' ? 'false' : 'true');
sw.addEventListener('click', toggle);
let t;
return {
play() { clearTimeout(t); sw.setAttribute('aria-checked', 'false'); t = setTimeout(() => sw.setAttribute('aria-checked', 'true'), 350); },
stop() { clearTimeout(t); sw.setAttribute('aria-checked', 'false'); },
};
}
Inspired by m3.material.io — Material 3 switch. The preview is an original re-creation; the effect name belongs to its author.
Two different timings on purpose: the thumb overshoots (feels physical), the colour just crossfades (overshoot on colour looks like a glitch). State lives in `aria-checked`, so the CSS and the screen reader read the same source.
Added 2026-11-09 · Report an issue