- Aria Patel@aria
- Tom Becker@tombecker
- Ren Okada@renokada
- Lena Fischer@lenaf
- Kofi Mensah@kofi
The shape of the problem
Removing an item from a list is one of those interactions that's trivial to make work and surprisingly easy to make feel cheap. The default version is a hard cut: the row vanishes, and everything below it jumps up to fill the space. It works, but the jump is the kind of small ugliness that quietly tells the user nobody looked too closely.
What I want instead is for the row to leave. To acknowledge it's going, take a beat to do it, and let the rest of the list flow up to meet the space rather than snapping into it.
What's actually animating
Three things happen when a row is removed, and the trick is that they're genuinely simultaneous rather than sequenced:
- The row blurs and shrinks. It fades to
opacity: 0, scales down a touch to0.92, and picks up ablur(4px)filter. The blur is what makes it read as dissolving rather than just disappearing — it stops looking like a solid object the moment it starts to leave. - Its slot collapses. The same element animates
heightfromautoto0withoverflow: hidden, so the space it held closes up smoothly instead of being released all at once. - The rest reflow into the gap. Because the height collapse happens in normal document flow, every row below simply follows the shrinking edge upward. There's no separate "now move the others" animation to coordinate — they're just reacting to the slot closing above them.
All of it runs on the curve I reach for on most UI exits, cubic-bezier(0.23, 1, 0.32, 1), over 280ms. Long enough to register as deliberate, short enough that removing three in a row never feels like queuing.
The one detail that makes it work
The part that's easy to get wrong: the blur and scale live on the same element as the height collapse. My first instinct was to fade the content and collapse the container separately, but that gives you two animations fighting over the same 280ms, and the result reads as fussy. Putting all four properties — opacity, scale, blur, height — on one motion.li keeps them locked together, so the row reads as a single thing leaving rather than a box deflating around its contents.
What it's stripped of
A production version of this would usually have an undo affordance — a "removed, undo?" toast, the way Gmail handles archiving — because a dismiss this satisfying is also a dismiss that's easy to fire by accident. I've left that out so the animation is the whole story, and the reset button is only there so you can watch it again.
Softening a destructive action
A delete is destructive, and destructive actions are exactly where a little softness earns its place. The hard cut leaves the user faintly unsure whether the right thing left. The melt — the blur, the collapse, the rows sliding up — answers that before they can ask it. They saw the thing go, and they saw where it went.