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
Wi-Fi
- Home
- Office
- Guest
transform-origin: centerGrow toward the fingerGood
Wi-Fi
- Home
- Office
- Guest
transform-origin: far edge from fingerconst 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: pCards 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% → archiveArms before it commitsGood
icon grows, then sticks to the row edgeconst 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.