Drag to bin
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.
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.
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
- 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.
- Inside the ghost, split the card into a fixed-size handle and a visible card absolutely positioned within it.
useDraggablemoves only the handle. - Call
useDraggable(handle, { onGrab, onUpdate, onRelease }). InonGrab, measure the ghost's rest rect and the bin's rect once withgetBoundingClientRect(), and precompute the size delta between them. - In
onUpdate, test whether the handle's centre point falls inside the bin's rect, and report that boolean only when it changes. - Drive a
useAnimatablevalue from that boolean, from 0 to 1, and interpolate the card's width, height, and offset from it in a computed style. - Give the draggable
snap: [0]and areleaseEasespring so a card dropped outside the bin returns on its own. - 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.
- To restore an item, set the blend value back to
0and calldraggable.reset().
Using the nanime Nuxt module, build a Vue 3 draggable-to-bin list. For each
list item, render a ghost wrapper that stays in the document's normal flow
and holds a fixed-size handle element, with the visible card absolutely
positioned inside that handle.
Use useDraggable(handle, { onGrab, onUpdate, onRelease }) so only the handle
is the drag target. In onGrab, measure the handle's ghost rect and the bin
element's rect once with getBoundingClientRect(), and compute the delta
between the card's rest size and the bin's size.
In onUpdate, test whether the handle's centre point falls inside the bin's
rect and, only when that boolean changes, drive a useAnimatable value from
0 to 1 with outQuad easing over about 350ms. Bind the card's width, height,
and translate offset in a computed style interpolated from that animatable
value, so it shrinks and slides toward the bin as it's dragged over it.
Give the draggable snap: [0] and releaseEase: spring({ bounce: 0.2,
duration: 500 }) so a card dropped outside the bin springs back to its
origin without a release handler.
On release, if the boolean was true, call draggable.stop() and mark the item
as binned. Collapse the ghost's height and margin-bottom to 0 with a CSS
transition so the rest of the list reflows to close the gap. To restore an
item, set the animatable value back to 0 and call draggable.reset().