Drag and settle

드래그 앤 세틀

The pointer-driven descendant of follow-through and overlap: while dragging, the object trails the cursor on a slight elastic lag; on release, it keeps going on its own momentum, overshoots, and springs into place.

Also known as: Drag inertiaRelease and settle
···
html
<div class="stage">
  <div class="track"></div>
  <i class="ball" id="dball"></i>
  <i class="cursor" id="dcursor"></i>
</div>
css
.stage{position:absolute;inset:0}
.track{position:absolute;left:8%;right:8%;top:50%;height:2px;margin-top:-1px;background:var(--line)}
.ball{position:absolute;top:50%;--d:clamp(20px,9vmin,34px);width:var(--d);height:var(--d);
  margin-top:calc(var(--d) / -2);margin-left:calc(var(--d) / -2);
  border-radius:50%;background:var(--accent);box-shadow:0 4px 10px rgba(0,0,0,.2)}
.cursor{position:absolute;top:50%;--d:clamp(12px,5vmin,20px);width:var(--d);height:var(--d);
  margin-top:calc(var(--d) / -2);margin-left:calc(var(--d) / -2);
  border-radius:50%;background:var(--fg);opacity:0}
js
const ball = document.getElementById('dball');
const cursor = document.getElementById('dcursor');
const START = 0.12, TARGET = 0.72;
const GRAB_AT = 500, DRAG_END = 1900, CYCLE = 3400;
let ballX = START, vx = 0;
const startT = performance.now();
function frame(now) {
  const t = (now - startT) % CYCLE;
  if (t < 20) { ballX = START; vx = 0; }
  let goal, cursorX, cursorOpacity;
  if (t < GRAB_AT) {
    const p = t / GRAB_AT;
    cursorX = -0.08 + (START - -0.08) * p;
    cursorOpacity = Math.min(p * 3, 1);
    goal = START;
  } else if (t < DRAG_END) {
    const p = (t - GRAB_AT) / (DRAG_END - GRAB_AT);
    cursorX = START + (TARGET - START) * p;
    cursorOpacity = 1;
    goal = cursorX;
  } else {
    cursorX = TARGET;
    cursorOpacity = Math.max(1 - (t - DRAG_END) / 260, 0);
    goal = TARGET;
  }
  cursor.style.left = (cursorX * 100) + '%';
  cursor.style.opacity = String(cursorOpacity);

  const k = 0.05, damp = 0.8;
  vx = (vx + (goal - ballX) * k) * damp;
  ballX += vx;
  ball.style.left = (ballX * 100) + '%';
  const stretch = Math.min(Math.abs(vx) * 22, 0.45);
  ball.style.transform = 'scaleX(' + (1 + stretch) + ') scaleY(' + (1 - stretch * 0.6) + ')';

  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Classic follow-through and overlapping action were pre-drawn sequences, but a drag is live user input with no predetermined in-betweens. So modern UI fakes the same feeling with a real-time spring simulation: every frame, it measures the gap between "target" (the cursor, or the final resting spot) and "current position," and nudges position toward it by a force proportional to that gap.

While dragging, the target is the cursor itself, so the object never snaps exactly onto it — it trails a beat behind, on an elastic lag. The instant the pointer releases, the target switches to the final resting position, but the velocity built up until then doesn't vanish — the object overshoots past the target and springs back, settling with a decaying wobble.

iOS sheets, Trello's card drag, and macOS window snapping all rely on this feeling — not that the numbers land exactly right, but that something was physically grabbed, carried, and let go.

In the demo, a fake cursor grabs the ball and drags it to the right. The ball trails a step behind the cursor the whole time (lag during the drag); the moment the cursor lets go, the ball's own momentum carries it a little further before it springs back and settles (overshoot and settle after release).

When to use

Use it for real drags, swipes, and bottom sheets — anything with a moment the user physically lets go. A single click doesn't need a spring settle; ordinary easing is enough.