Accordion expandWeb · App
Target
Implement the "Spring press button" effect in vanilla HTML/CSS/JS. Context: Button squashes on press and springs back with a slight overshoot on release. Trigger: the element is pressed (pointerdown → pointerup). Timeline: time-based (duration + easing). Motion tokens (normal): - scale: 0.94 (while pressed) → 1 (release) - duration: 400ms - easing: cubic-bezier(0.34, 1.56, 0.64, 1) - note: press-in uses 120ms ease-out; release uses the overshoot curve Motion tokens (prefers-reduced-motion): - opacity: 1 → 0.8 (while pressed) - duration: 120ms - easing: ease-out 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 - works with keyboard activation (Space/Enter) as well as pointer - no change to layout size of neighbours
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-spring-press-button" data-fx-root> <button type="button" class="fx-btn">Add to cart</button> </div>
demo.css
.fx-spring-press-button { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-spring-press-button .fx-btn {
height: 44px; padding: 0 22px; border: 0; border-radius: 10px; cursor: pointer;
background: var(--fx-ink, #18181b); color: #fff; font: 500 14px/1 Inter, system-ui, sans-serif;
box-shadow: 0 8px 24px rgba(24, 24, 27, .18);
transition: transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.fx-spring-press-button .fx-btn:active,
.fx-spring-press-button .fx-btn.is-pressed { transform: scale(0.94); transition-duration: 120ms; transition-timing-function: ease-out; }
@media (prefers-reduced-motion: reduce) {
.fx-spring-press-button .fx-btn { transition: opacity 120ms ease-out; }
.fx-spring-press-button .fx-btn:active,
.fx-spring-press-button .fx-btn.is-pressed { transform: none; opacity: .8; }
}
demo.js
export default function mount(root) {
const btn = root.querySelector('.fx-btn');
let t;
return {
// "play" simulates a press so the list card can demo it without a pointer
play() { clearTimeout(t); btn.classList.add('is-pressed'); t = setTimeout(() => btn.classList.remove('is-pressed'), 160); },
stop() { clearTimeout(t); btn.classList.remove('is-pressed'); },
};
}
Inspired by developer.apple.com — iOS button press feedback. The preview is an original re-creation; the effect name belongs to its author.
`:active` does the real work; `.is-pressed` exists only so the gallery can replay without a pointer.
Added 2026-11-09 · Report an issue