Examples

Drag to bin

Dragging, hit testing, and a list that closes its own gap.

Drag a card over the bin and it settles into place, or pull it back out and it springs back to its original size. Let go while it's over the bin and the card fades out, its slot in the list collapses, and everything below slides up to close the gap.

Design system
Nuxt SSR
Anime.js v4
Tailwind CSS
Drop here
4 left
View on GitHub

What's happening

Drag a card toward the bin and it shrinks and slides as it gets close, like it's being pulled in. Drop it inside and it fades out, and the rest of the list closes up the space it left. Pull it back out before letting go, and it snaps back to its normal size, like nothing happened.

How it's built

useDraggable moves the card, and it also handles the return trip. snap: [0] and releaseEase: spring({ bounce: 0.2, duration: 500 }) mean a card released outside the bin springs back to its origin on its own, with no release handler involved.

The shrink toward the bin runs off useAnimatable. Crossing into the bin is treated as a state rather than a gradient: onUpdate tests the handle's centre against the bin's box and reports a boolean, and the animatable eases a single 0 → 1 value from it over 350ms. The card's width, height and offset are all interpolated from that value in a computed style, so the "inside the bin" look is one animation rather than three.

Everything else is CSS. The bin's highlight is a class toggled from the same boolean, the binned card fades on an opacity transition, and the slot it occupied collapses through a height and margin transition on the ghost element. The rest of the list is not animated at all: it moves because the collapsing ghost is in normal document flow, so the browser reflows what sits below it.

Nothing is positioned by hand. Each card sits inside a ghost element that holds its place in the flow, and the card itself is absolutely positioned within that ghost, so dragging never disturbs the layout around it.

The card is also split in two. A fixed-size handle is what useDraggable moves, and the visible card inside it is what changes shape. That keeps the drag target's own box constant, which matters because AnimeJS watches the draggable element with a ResizeObserver and schedules a refresh whenever its size changes.

On release the only work is bookkeeping. If the card was over the bin, draggable.stop() halts it and the parent marks the item binned. Restoring an item resets the blend value and calls draggable.reset() to put the handle back at its origin.

Geometry is measured once in onGrab rather than on every frame. Nothing in the list moves until the card is released, so a per-frame getBoundingClientRect() would buy nothing and cost a layout on every tick.

Building it yourself

  1. Wrap each item in a ghost element that stays in normal document flow. This is what preserves list position and lets the list reflow when a card is removed.
  2. Inside the ghost, split the card into a fixed-size handle and a visible card absolutely positioned within it. useDraggable moves only the handle.
  3. Call useDraggable(handle, { onGrab, onUpdate, onRelease }). In onGrab, measure the ghost's rest rect and the bin's rect once with getBoundingClientRect(), and precompute the size delta between them.
  4. In onUpdate, test whether the handle's centre point falls inside the bin's rect, and report that boolean only when it changes.
  5. Drive a useAnimatable value from that boolean, from 0 to 1, and interpolate the card's width, height, and offset from it in a computed style.
  6. Give the draggable snap: [0] and a releaseEase spring so a card dropped outside the bin returns on its own.
  7. On release, if the boolean was true, stop the draggable and mark the item as binned. Collapse the ghost's height and margin with a CSS transition so the list closes the gap.
  8. To restore an item, set the blend value back to 0 and call draggable.reset().
Copyright © 2026