Hint toward the gesture

In-between frames lean toward where the gesture is heading.

From apple-design, §8 Hint in the direction of the gesture.

Press and hold to expand

Hold a tile near a corner, then drag while holding.

Scale from centerBad
transform-origin: center
Grow toward the fingerGood
transform-origin: far edge from finger

const r = tile.getBoundingClientRect();
const dx = finger.x - (r.left + r.width / 2);
const dy = finger.y - (r.top + r.height / 2);
const len = Math.hypot(dx, dy) || 1;
const w = Math.min(1, len / (r.width / 2));
const reach = Math.min(len * 0.2, 14) * p;
tile.style.transformOrigin = `${50 - (dx / len) * 50 * w}% ${50 - (dy / len) * 50 * w}%`;
tile.style.transform =
  `translate(${(dx / len) * reach}px, ${(dy / len) * reach}px) scale(${1 + 0.14 * p})`;

// FLIP from the tile's current rect so the motion continues.
const t = tile.getBoundingClientRect(), s = panel.getBoundingClientRect();
panel.animate([
  { transform: `translate(${t.left - s.left}px, ${t.top - s.top}px) scale(${t.width / s.width}, ${t.height / s.height})` },
  { transform: "none" },
], { duration: 300, easing: "cubic-bezier(0.23, 1, 0.32, 1)" });

Pull to open a stack

Drag a stack down slowly and stop halfway.

Crossfades statesBad
stacked: 1 - p · listed: p
Cards track the fingerGood
translateY(lerp(stacked, listed, p))

const H = 56, GAP = 8, RANGE = 2 * (H + GAP);
function frame(p) {
  cards.forEach((card, i) => {
    const y = lerp(i * 7, i * (H + GAP), p);
    const s = lerp(1 - i * 0.05, 1, p);
    card.style.transform = `translateY(${y}px) scale(${s})`;
  });
}
onDrag((dy) => frame((p0 * RANGE + rubber(dy)) / RANGE));
onRelease((p, v) => spring({ from: p, to: v > 1.2 || p > 0.4 ? 1 : 0, velocity: v, onUpdate: frame }));

Swipe to archive

Swipe a row left past halfway, then back.

Reveal onlyBad
release > 50% → archive
Arms before it commitsGood
icon grows, then sticks to the row edge

const revealed = -x;
const armed = revealed > row.offsetWidth * 0.5;
row.classList.toggle("is-armed", armed);
if (armed !== wasArmed) pulse.animate(
  [{ transform: "scale(1)" }, { transform: "scale(1.2)" }, { transform: "scale(1)" }],
  { duration: 200, easing: "ease-out" });
icon.style.transform = armed
  ? `translateX(${Math.min(0, ICON - revealed)}px)`
  : `scale(${0.5 + 0.5 * Math.min(1, revealed / ICON)})`;
icon.style.opacity = Math.min(1, revealed / 48);

Why it matters, pros and cons

Why it matters

Leaning frames show the outcome while it can still be undone.

Pros

  • Outcome readable mid-gesture.
  • Cause and effect stay tied to the finger.
  • An armed state can replace a confirm dialog.

Cons

  • Keyboard has no finger; aim at the destination.
  • Off-center scaling distorts content past about 15%.
  • Skip for non-spatial changes like toggles.