Drag like a physical object
Dismiss on velocity, damp past edges, ignore extra touches.
Emil Kowalski@emilkowalskiYou can now swipe toasts to dismiss in Linear. Using Sonner under the hood
Emil Kowalskiemilkowal.skiIf the swipe amount is greater than the threshold or velocity is higher than in this case 0.11, I remove the toast.
Emil Kowalskiemilkowal.ski
Emil Kowalskiemilkowal.skiWhen you drag upwards while already at the top of the drawer, the drawer will damp the drag, meaning the more you drag, the less the drawer will move.
Emil Kowalskiemilkowal.skiInstead of just not allowing the toast to be dragged upwards, you can still drag it, but it will slow down and eventually stop.
Emil Kowalskiemilkowal.skiOnce I start dragging, I set the toast to capture all future pointer events. This ensures that even if the mouse or your thumb moves outside the toast while dragging, the toast remains the target of the pointer events.
Emil Kowalskiemilkowal.skiWe simply ignore all touches after the initial one until the user releases to prevent this
From emil-design-eng and his Sonner and Vaul write-ups.
A flick is enough
Flick each toast right, short and fast.
Distance onlyBad
Invoice sentSwipe right to dismiss
0px / 0ms = 0.00 px/ms
Distance or velocityGood
Invoice sentSwipe right to dismiss
0px / 0ms = 0.00 px/ms
const velocity = Math.abs(swipeAmount) / timeTaken;
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
dismiss();
} else {
snapBack();
}
Damp past the edge
Pull each sheet up past its top, then let go.
Hard stopBad
Payment details
finger 0px · sheet 0px
DampedGood
Payment details
finger 0px · sheet 0px
const damp = (overshoot, size, c = 0.55) =>
size * (1 - 1 / ((overshoot * c) / size + 1));
const y = dragY >= 0 ? dragY : -damp(-dragY, stageHeight);
sheet.style.transform = `translate3d(0, ${y}px, 0)`;
Hold on to the first pointer
Drag each knob right, fast, or simulate a second finger.
Last touch winsBad
gap 0px
First touch onlyGood
gap 0px
el.addEventListener("pointerdown", (e) => {
if (activeId !== null) return;
activeId = e.pointerId;
el.setPointerCapture(e.pointerId);
});
Why it matters, pros and cons
Why it matters
Speed carries intent: a fast flick means “go away”. A hard clamp feels like a bug.
Pros
- A 60px flick dismisses; no long drag.
- Damping signals the end without a jolt.
- Capture keeps the drag alive off-element.
Cons
- Too low a threshold dismisses by accident.
- Damping can hide a limit a hard stop shows.
- Pinch and rotate need the extra touches.