GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Button Press Animation UI: 9 Calculation Patterns

One value overshoots its target and swings back before settling, one falls and bounces off a floor value before resting, and one snaps toward its nearest

Auto-plays · click a tile to jump to its section · all nine in one zip

All nine items answer to a real pointer input and nothing else — five (02, 04, 05, 07, 09) track a drag, two (01, 06) track a press-and-hold, one (03) answers a click, and one (08) answers only the release, for nine real listeners and zero autoplay-only demos. None of the nine calculates its motion from Math.random — every curve above comes from a deterministic requestAnimationFrame loop reading fixed constants, so the same input sequence always produces the same numbers, frame for frame. That determinism is what separates a real button press animation css loop like these nine from a transition: transform 0.3s ease shortcut — the numbers below come from watching the actual values change, not from guessing at a curve.

Anatomy of nine patterns — which value changes and when

Nine buttons, nine different answers to the same question: once a value starts moving, what decides when and how it stops?

Why five patterns integrate an acceleration into a velocity into a position every frame

01, 03, 05, 06, and 09 all run the same two-line idiom inside their tick() loop — a velocity value accumulates an acceleration value, then a position value accumulates that velocity — and only the acceleration formula changes between them. 01, 06, and 09 compute that acceleration from a spring-damper equation pulling toward a target value; 05 computes it from a pendulum's restoring torque; 03 replaces the pull with a constant downward acceleration and adds a bounce whenever the position value crosses a floor value.

Why two patterns move a value by a fixed ratio each frame instead of computing a restoring force

02 and 07 never touch an acceleration value at all. 02 narrows the gap between a target position value and the button's own position value by a fixed ratio every frame; 07 instead shrinks an inherited speed value by a fixed ratio every frame and lets that shrinking speed carry the position along. Neither ever computes a force from a mass value.

Why two patterns read a value straight off a closed-form curve instead of running an integrator

04's drag phase and 08's wobble never accumulate anything frame to frame — 04 maps the pointer's distance value through a fixed exponential curve to get a stretch value, and 08 plugs elapsed time straight into a decaying sine formula to get an angle value. Both recompute their output from scratch on every frame rather than carrying a running velocity forward (04 does switch to the shared spring idiom once you release, to pull its stretch value back to 1).

Code per pattern

01Press-value overshoot — the pressed value passes its target value and swings back, repeating while the swing shrinks

Pressing the button sends its depth value past the target depth and back several times, with each swing shrinking until it settles flat. A form's submit button or a confirm dialog's primary action reads this way, since the small rebound signals the press registered without slowing the response down.

Press-value overshoot — the pressed value passes its target value and swings back, repeating while the swing shrinks
Open in a new tab

01 · Press-value overshoot — the pressed value passes its target value and swings back, repeating while the swing shrinks — Measured area 7.131% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

KCPRESS_DEPTH

What the numbers say

Measured across 23 tracked frames, 01 shifts on all 23 of them, spanning a 7.13% cumulative area at a 136.0 average intensity — the constant back-and-forth of an underdamped spring keeps nearly every frame in motion.

  // Computed layer: spring-damper equation accel = -(K/MASS)*(depth-target) - (C/MASS)*vel.
  // The overshoot that passes the target and swings back happens because the damping ratio
  // (zeta = C/(2*sqrt(K*MASS))) sits under 1 (underdamped) — a lower zeta means a bigger swing.
  var K = 300, C = 7, MASS = 1, PRESS_DEPTH = 14;
  var depth = 0, vel = 0, target = 0, last = 0, raf = null;

  function paint() {
    var scale = 1 - depth / 340;
    btn.style.transform = 'translateY(' + depth.toFixed(2) + 'px) scale(' + scale.toFixed(4) + ')';
    val.textContent = depth.toFixed(1) + 'px';
  }
  function tick(now) {
    var dt = Math.min((now - last) / 1000, 0.032);
    last = now;
    var accel = (-K * (depth - target) - C * vel) / MASS;
    vel += accel * dt;
    depth += vel * dt;
    paint();
    raf = requestAnimationFrame(tick);
  }

02Trailing-value interpolation — the button's position value follows the pointer's position value, narrowing the gap by a fixed ratio each moment

Dragging the floating button moves a target position value instantly, but the button's own position value only narrows the gap between the two by a fixed ratio every frame, so it visibly trails behind. A draggable floating action button or a freely repositioned toolbar button uses this lag to read as weighted rather than glued to the pointer.

Trailing-value interpolation — the button's position value follows the pointer's position value, narrowing the gap by a fixed ratio each moment
Open in a new tab

02 · Trailing-value interpolation — the button's position value follows the pointer's position value, narrowing the gap by a fixed ratio each moment — Measured area 10.234% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LERP_RATIOFAB_RtargetX

What the numbers say

Across the same 23-frame measurement, 02 moves on all 23, covering a 10.23% cumulative area at a 156.8 average intensity — a continuously trailing chase keeps the button drifting even between pointer moves.

  // Computed layer: each frame narrows the gap between the pointer's position value
  // (targetX/Y) and the button's position value (x/y) by only LERP_RATIO —
  // x += (targetX-x)*LERP_RATIO. Closer to 1 catches up almost instantly; lower stretches the trail out.
  var LERP_RATIO = 0.14;
  var FAB_R = 26;
  var x = 0, y = 0, targetX = 0, targetY = 0, raf = null;

  function bounds() { return { w: field.clientWidth, h: field.clientHeight }; }
  function clampPos(px, py) {
    var b = bounds();
    return [Math.max(FAB_R, Math.min(b.w - FAB_R, px)), Math.max(FAB_R, Math.min(b.h - FAB_R, py))];
  }
  function paint() { fab.style.transform = 'translate(' + (x - FAB_R).toFixed(1) + 'px,' + (y - FAB_R).toFixed(1) + 'px)'; }
  function tick() {
    x += (targetX - x) * LERP_RATIO;
    y += (targetY - y) * LERP_RATIO;
    paint();
    raf = requestAnimationFrame(tick);
  }

03Drop-and-settle rebound — the position value falls, bounces off a floor value repeatedly, and comes to rest

Triggering the drop sends the position value falling under a constant acceleration until it reaches the floor value, bounces back up, and repeats with a shrinking height each time until it rests. A badge or highlighted button that appears on screen entry uses this fall-and-settle motion to draw a first glance without looping forever.

Drop-and-settle rebound — the position value falls, bounces off a floor value repeatedly, and comes to rest
Open in a new tab

03 · Drop-and-settle rebound — the position value falls, bounces off a floor value repeatedly, and comes to rest — Measured area 11.576% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYRESTITUTIONSTOP_VEL

What the numbers say

03 moves on 17 of the 23 measured frames, covering an 11.58% cumulative area at a 102.3 average intensity — the lower frame count reflects the gaps between bounces, where the chip sits still at the top of each arc.

  // Computed layer: vel += GRAVITY*dt, y += vel*dt drops the value until it reaches
  // the floor value, then flips vel by -RESTITUTION to bounce it. Once |vel| falls below STOP_VEL, it settles.
  var GRAVITY = 2200, RESTITUTION = 0.52, STOP_VEL = 40, CHIP_H = 34;
  var y = 0, vel = 0, last = 0, raf = null, running = false;

  function floorY() { return field.clientHeight - CHIP_H; }
  function paint() { chip.style.transform = 'translateY(' + y.toFixed(1) + 'px)'; }
  function tick(now) {
    var dt = Math.min((now - last) / 1000, 0.032);
    last = now;
    vel += GRAVITY * dt;
    y += vel * dt;
    var floor = floorY();
    if (y >= floor) {
      y = floor;
      vel = -vel * RESTITUTION;
      if (Math.abs(vel) < STOP_VEL) { vel = 0; paint(); running = false; return; }
    }
    paint();
    raf = requestAnimationFrame(tick);
  }

04Pull-distance stretch — the button's shape value stretches by an amount tied to how far the pointer's distance value has moved away, then returns when released

Dragging away from the button feeds the pointer's distance value into a curve that stretches the button's shape value further the more you pull, though the growth flattens out rather than climbing without limit; releasing lets a spring pull the shape back to its original value. A pull-to-refresh control at the top of a list or a slide-to-confirm payment button relies on that resistance to signal how far is enough.

Pull-distance stretch — the button's shape value stretches by an amount tied to how far the pointer's distance value has moved away, then returns when released
Open in a new tab

04 · Pull-distance stretch — the button's shape value stretches by an amount tied to how far the pointer's distance value has moved away, then returns when released — Measured area 3.297% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

PULL_RESISTANCEMAX_STRETCHstretchFromDist

What the numbers say

04 moves on 19 of 23 frames, covering a 3.30% cumulative area at a 208.5 average intensity — the smallest cumulative area of the nine, since only the button's own outline changes shape while the rest of the scene stays still, but the highest average intensity, since that single shape change is a sharp per-pixel edge.

  // Dividing by PULL_RESISTANCE and applying 1-exp(-x) builds a rubber-band resistance
  // curve whose growth shrinks as distance grows (stiffer the further you pull). Releasing lets the spring (K, C) pull it back to 1.
  var PULL_RESISTANCE = 130, MAX_STRETCH = 0.9, K = 260, C = 8;
  var dist = 0, stretch = 1, vel = 0, target = 1, dragging = false, startX = 0, raf = null, last = 0;

  function stretchFromDist(d) {
    var n = Math.max(0, d) / PULL_RESISTANCE;
    return 1 + MAX_STRETCH * (1 - Math.exp(-n));
  }
  function paint() { btn.style.transform = 'scaleX(' + stretch.toFixed(3) + ')'; }
  function springTick(now) {
    var dt = Math.min((now - last) / 1000, 0.032);
    last = now;
    var accel = -K * (stretch - target) - C * vel;
    vel += accel * dt;
    stretch += vel * dt;
    paint();
    if (Math.abs(stretch - target) < 0.002 && Math.abs(vel) < 0.01) { stretch = target; vel = 0; paint(); raf = null; return; }
    raf = requestAnimationFrame(springTick);
  }

05Hanging-angle swing — a value hung from a top point swings its angle value side to side while the swing shrinks over time

Pulling the button sideways and letting go sends its angle value swinging back and forth around a fixed pivot, with the width of each swing shrinking over time until it hangs still. A notification button that looks hung from the top of a menu or an action button in a decorative header uses the swing to read as physically attached rather than pinned in place.

Hanging-angle swing — a value hung from a top point swings its angle value side to side while the swing shrinks over time
Open in a new tab

05 · Hanging-angle swing — a value hung from a top point swings its angle value side to side while the swing shrinks over time — Measured area 6.13% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYARM_LENDAMPING

What the numbers say

05 matches 01 and 02 at 23 of 23 frames in motion, covering a 6.13% cumulative area at a 168.3 average intensity — the pendulum's own periodic overshoot past its resting angle keeps it swinging through nearly the entire measurement window.

  // Computed layer: angular acceleration = -(GRAVITY/ARM_LEN)*sin(angle) - DAMPING*avel. Since angle is in degrees,
  // it's converted to radians before sin() and back to degrees after (the RAD constant) — skip that conversion
  // and the restoring force shrinks by a factor of 57, making the swing look like it has already stopped.
  var GRAVITY = 2000, ARM_LEN = 110, DAMPING = 0.42, RAD = 180 / Math.PI;
  var angle = 0, avel = 0, dragging = false, last = 0, running = false;

  function paint() { arm.style.transform = 'rotate(' + angle.toFixed(2) + 'deg)'; }
  function tick(now) {
    var dt = Math.min((now - last) / 1000, 0.032);
    last = now;
    avel += (-(GRAVITY / ARM_LEN) * Math.sin(angle / RAD) * RAD - DAMPING * avel) * dt;
    angle += avel * dt;
    paint();
    if (Math.abs(angle) < 0.2 && Math.abs(avel) < 0.6) { angle = 0; avel = 0; paint(); running = false; return; }
    raf = requestAnimationFrame(tick);
  }

06Mass-value press-speed difference — the larger a preset mass value is, the slower the pressed-and-return speed value becomes

Three buttons run the identical spring equation with the identical spring and damping constants, but each carries its own preset mass value, so the heavier one presses in and springs back visibly slower than the lighter one under the same force. A delete button designed to feel heavy or a payment button meant to encourage a deliberate press leans on that slower response to ask for a moment of hesitation.

Mass-value press-speed difference — the larger a preset mass value is, the slower the pressed-and-return speed value becomes
Open in a new tab

06 · Mass-value press-speed difference — the larger a preset mass value is, the slower the pressed-and-return speed value becomes — Measured area 14.093% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

MASSKPRESS_DEPTH

What the numbers say

06 posts the highest cumulative area of the nine at 14.09%, with all 23 frames in motion and a 136.1 average intensity — three independent springs animating in the same frame stack their moved-pixel areas on top of each other.

  var MASS = { light: 0.5, medium: 1.4, heavy: 3.6 };

  function makeBtn(el, key) {
    return { el: el, mass: MASS[key], depth: 0, vel: 0, target: 0 };
  }
  var items = [
    makeBtn(document.getElementById('bwmLight'), 'light'),
    makeBtn(document.getElementById('bwmMedium'), 'medium'),
    makeBtn(document.getElementById('bwmHeavy'), 'heavy')
  ];
  var last = 0, raf = null;

  function paint(it) { it.el.style.transform = 'translateY(' + it.depth.toFixed(2) + 'px)'; }
  function tick(now) {
    var dt = Math.min((now - last) / 1000, 0.032);
    last = now;
    for (var i = 0; i < items.length; i++) {
      var it = items[i];
      var accel = (-K * (it.depth - it.target) - C * it.vel) / it.mass;
      it.vel += accel * dt;
      it.depth += it.vel * dt;
      paint(it);
    }
    raf = requestAnimationFrame(tick);
  }

07Thrown-speed decay move — motion continues at the speed value captured at release, with that value shrinking over time until it stops

Flicking the button hands its motion the exact speed value captured at the moment of release, and that speed value shrinks exponentially every frame until the button coasts to a stop. A floating button flicked away with a light throw or an action button that slides on after a swipe both rely on that decay to read as momentum rather than a scripted slide.

Thrown-speed decay move — motion continues at the speed value captured at release, with that value shrinking over time until it stops
Open in a new tab

07 · Thrown-speed decay move — motion continues at the speed value captured at release, with that value shrinking over time until it stops — Measured area 5.038% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

FRICTIONSTOP_VELCHIP_R

What the numbers say

07 moves on 21 of 23 frames, covering a 5.04% cumulative area at a 161.8 average intensity — the coast comes to rest a beat before the measurement window ends, leaving the last couple of frames still.

  // That speed value itself is shrunk exponentially by the FRICTION ratio — vel *= FRICTION**dt.
  // Hitting a wall zeroes only that axis's speed, so it stops right where it landed.
  var FRICTION = 0.05, STOP_VEL = 6, CHIP_R = 24;
  var x = 100, y = 60, vx = 0, vy = 0, last = 0, raf = null;
  var dragging = false, samples = [];

  function bounds() { return { w: field.clientWidth, h: field.clientHeight }; }
  function paint() { chip.style.transform = 'translate(' + (x - CHIP_R).toFixed(1) + 'px,' + (y - CHIP_R).toFixed(1) + 'px)'; }
  function tick(now) {
    var dt = Math.min((now - last) / 1000, 0.032);
    last = now;
    var decay = Math.pow(FRICTION, dt);
    vx *= decay; vy *= decay;
    x += vx * dt; y += vy * dt;
    var b = bounds();
    if (x < CHIP_R) { x = CHIP_R; vx = 0; }
    if (x > b.w - CHIP_R) { x = b.w - CHIP_R; vx = 0; }
    if (y < CHIP_R) { y = CHIP_R; vy = 0; }
    if (y > b.h - CHIP_R) { y = b.h - CHIP_R; vy = 0; }
    paint();
    if (Math.abs(vx) < STOP_VEL && Math.abs(vy) < STOP_VEL) { vx = 0; vy = 0; raf = null; return; }
    raf = requestAnimationFrame(tick);
  }

08Post-release wobble decay — the shape value wobbles side to side from the moment of release, and the wobble's width value shrinks until it stops

Releasing the button plugs the elapsed time directly into a decaying sine formula, so the shape value wobbles left and right immediately after release and the wobble narrows on its own, with no per-frame velocity ever tracked at all. A hold-to-confirm button or a retry button signaling an error state both use that immediate wobble to mark the exact instant of release.

Post-release wobble decay — the shape value wobbles side to side from the moment of release, and the wobble's width value shrinks until it stops
Open in a new tab

08 · Post-release wobble decay — the shape value wobbles side to side from the moment of release, and the wobble's width value shrinks until it stops — Measured area 1.779% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

AMPLITUDE0DECAYFREQ

What the numbers say

08 has the fewest frames in motion of the nine at 9 of 23, covering a 1.78% cumulative area at a 115.0 average intensity — the exponential decay in its formula drives the wobble below the visible threshold well before the window closes.

  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // Computed layer: angle(t) = AMPLITUDE0 * e^(-DECAY*t) * sin(FREQ*t) — an exponentially decaying
  // sine wave computes the left-right rotation angle directly (a value assigned every frame, not a keyframe).
  // A bigger DECAY dies out faster; a bigger FREQ wobbles more tightly.
  var AMPLITUDE0 = 9, DECAY = 5.2, FREQ = 26;
  var t0 = 0, raf = null, running = false;

  function paint(angle) { btn.style.transform = 'rotate(' + angle.toFixed(2) + 'deg)'; }
  function tick(now) {
    var t = (now - t0) / 1000;
    var angle = AMPLITUDE0 * Math.exp(-DECAY * t) * Math.sin(FREQ * t);
    paint(angle);
    if (Math.abs(angle) < 0.15 && t > 0.15) { paint(0); running = false; raf = null; return; }
    raf = requestAnimationFrame(tick);
  }

09Snap-to-anchor bounce — the released position value is pulled toward the nearest of several preset anchor values and settles with a bounce

Dragging and releasing the button picks whichever of three fixed anchor values sits closest to the release position as the new target, and the same spring equation as 01 and 06 pulls it there with a small overshoot-and-settle bounce. A toggle button that rests in one of several positions or a widget button that aligns to a grid both use that snap to make the resting spots feel inevitable rather than arbitrary.

Snap-to-anchor bounce — the released position value is pulled toward the nearest of several preset anchor values and settles with a bounce
Open in a new tab

09 · Snap-to-anchor bounce — the released position value is pulled toward the nearest of several preset anchor values and settles with a bounce — Measured area 6.549% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

ANCHORSKnearestAnchor

What the numbers say

09 moves on 18 of 23 frames, covering a 6.55% cumulative area at a 111.8 average intensity — motion pauses only in the instant right after release, before the nearest-anchor calculation has picked a target to spring toward.

  // Computed layer: from the released position value (x), the nearest fixed anchor value
  // (one of ANCHORS) is chosen as the target, and the spring (K, C) snaps it into place with a small overshoot-and-return.
  var ANCHORS = [30, 110, 190], K = 340, C = 10, BTN_R = 20;
  var x = ANCHORS[0], vel = 0, target = ANCHORS[0], dragging = false, startX = 0, startBtnX = 0, raf = null, last = 0;

  function nearestAnchor(px) {
    var best = ANCHORS[0], bestD = Infinity;
    for (var i = 0; i < ANCHORS.length; i++) {
      var d = Math.abs(ANCHORS[i] - px);
      if (d < bestD) { bestD = d; best = ANCHORS[i]; }
    }
    return best;
  }
  function paint() { btn.style.transform = 'translateX(' + x.toFixed(1) + 'px)'; }
  function tick(now) {
    var dt = Math.min((now - last) / 1000, 0.032);
    last = now;
    var accel = -K * (x - target) - C * vel;
    vel += accel * dt;
    x += vel * dt;
    paint();
    if (Math.abs(x - target) < 0.05 && Math.abs(vel) < 0.4) { x = target; vel = 0; paint(); raf = null; return; }
    raf = requestAnimationFrame(tick);
  }

Where values reshape a pattern — decay, mass, and boundary criteria

Why 01's damping constant decides whether the press overshoots or settles flat, not how deep it presses

Raising 01's damping constant C from 7 toward critical damping does not change PRESS_DEPTH or how far the button travels — it changes only whether the depth value swings past the target and back (underdamped) or eases straight in without crossing it (overdamped or critical).

Why 05's damping constant decides how long the swing lasts, not how wide the first swing is

05's DAMPING constant is multiplied against the angular velocity every frame, so it never touches the peak angle a single pull produces — it only decides how many times the pendulum crosses zero before avel drops low enough to stop, stretching a two-swing settle into a ten-swing one at a lower value.

Why 09's spring constant decides how fast the snap happens, not which anchor gets picked

09's K constant is applied only after nearestAnchor() has already chosen a target from the ANCHORS array — raising or lowering K changes how briskly the position value closes the gap to that already-chosen anchor, never which of the three anchors wins.

These three constants also mark where this post's nine patterns connect to others already covered here. Award-winning button interactions (236) drives its squash-press and magnet-pull items from CSS custom properties and transition timing (@property, translate) rather than from a per-frame acceleration-into-velocity-into-position loop the way these nine do. CTA button microinteraction UI patterns (352) computes its confetti-burst item's fall from a constant gravity value applied to scattered particles rather than to the button itself, and randomness turns up in three of its own items rather than just one — a seeded generator drives its glitch-split item's offsets, while its confetti-burst and vu-meter-tap items each pull straight from Math.random() — against these nine's fully deterministic integrator, which calls neither. Verlet softbody physics (306) runs every one of its nine scenes through Verlet integration, deriving each next position from the current and previous positions instead of storing an explicit velocity value the way these nine carry a vel variable — a real per-frame physics loop either way, just a different method for turning acceleration into motion. Grab the nine files as one archive if you want to test a button click animation css tweak locally before committing to it — unzip it with the password xuen7cg2 and every constant named above sits right there in the same index.html you just read.

Where it breaks — rapid taps, long drags, low-power devices

What happens when you press again before the spring has settled

01's and 06's pointerdown handler resets target to PRESS_DEPTH but never resets depth or vel first, so a second press fired while the first is still swinging back adds the new pull to whatever velocity is already in flight — a fast double-tap produces a visibly bigger overshoot than either single press does on its own, not a clean repeat of the same curve.

What happens when you drag far past where the curve was tuned for

04's stretchFromDist() runs the pointer's distance through 1-exp(-n), a curve that flattens hard as distance grows, so a drag three times past PULL_RESISTANCE looks almost identical to one only slightly past it — the resistance keeps rising, but the visible stretch stops telling them apart. 02's clampPos() catches the opposite case, holding the floating button's position value inside the field's bounds no matter how far outside it the pointer itself travels.

What happens on a slow or backgrounded device

Seven of the nine — 01, 03, 04, 05, 06, 07, and 09 — cap their per-frame dt at 0.032 seconds, so a long gap between frames (a backgrounded tab regaining focus, a dropped frame on a slow device) never injects a huge dt that would fling the integrator's position value far past where it should be. 02 has no such cap: its lerp runs once per rendered frame regardless of how much real time passed, so on a low frame rate it takes visibly longer in wall-clock time to catch up to the pointer than it does at 60fps. 08 sidesteps the question entirely, since it reads elapsed time straight from performance.now() rather than accumulating a per-frame dt, so its wobble plays out over the same real-world duration no matter the frame rate.

Three variants and accessibility

Item Constant Values What changes
01 press overshoot C 7 → 40 The same press still travels to the same depth, but a damping ratio near 1 removes the overshoot entirely, trading the springy rebound for a single firm stop
05 pendulum swing DAMPING 0.42 → 0.08 The same pull still starts the same first swing, but the pendulum crosses zero more than ten times instead of two or three before it comes to rest, staying visibly hung and swinging far longer
09 anchor snap K 340 → 120 The same release still targets the same nearest anchor, but the position value flows toward it over a noticeably longer, softer glide instead of snapping there almost immediately

None of the nine adds an aria-live region anywhere — real-time feedback here is always visual, never announced. 01, 06, and 08 carry no ARIA attribute at all, relying on a plain <button type="button"> and its visible content; 02's drag field and 05's swing field are each marked role="application" with an aria-label (05 also adds tabindex="0" for keyboard focus), while 03's floor marker, 04's anchor marker, and 05's pivot and rod are all aria-hidden="true" as purely decorative. 02, 05, 07, and 09's interactive elements each carry an aria-label describing what they do. Every item also checks prefers-reduced-motion: reduce and, when it matches, skips its own autoplay preview loop and snaps the interacted value straight to its resting state — 07 zeroes both velocity components so a flick produces no travel at all, 08 paints a wobble angle of exactly zero so a release produces no shake, and the other seven jump straight to the pressed, dropped, stretched, swung, or snapped resting value without animating through it.

FAQ

Does a button press animation like this need a physics engine or library?

No — all nine run on a plain requestAnimationFrame loop doing ordinary arithmetic on a handful of numbers (a position, a velocity, a handful of constants); nothing here imports a physics engine, and none of the nine files pulls in anything beyond the browser's own APIs.

Can the overshoot pattern (01) and the anchor-snap pattern (09) share one calculation?

They already do — 09 reuses the exact same spring-damper formula as 01 (and 06), just with an extra step first: nearestAnchor() picks which fixed value the spring should pull toward before the same accel/vel/position math takes over.

Do any of these nine read a real device sensor, like an accelerometer, for the press feel?

No — every value above comes only from pointer coordinates (clientX/clientY) and the fixed constants declared at the top of each file; none of the nine touches a device motion sensor, a gamepad, or any external data.

Sources: MDN — transition · MDN — easing-function · MDN — requestAnimationFrame · MDN — Pointer events · MDN — prefers-reduced-motion · MDN — pointerdown event

Enter the archive password

The password is inside this article. You will find it as you read.