Aave's share menu
A menu that flows like liquid.
Work in progress
This article is still being written and may change.
Aave's design site has a share button where the menu flows out of the button like liquid - a soft blob stretching down, then pinching off into the menu, while the labels stay perfectly still. This is the real thing, built the way they build it: a frosted-glass surface whose outline is a metaball, regenerated every frame.
Click Share
The whole effect is one element with an animated clip-path: shape(). You can't tween a shape() between two outlines (closed is one blob, open is two), so it isn't tweened - it's recomputed from a single openness value every frame. The trick is generating that outline from a field instead of by hand.
A field, not a path
Describe the button and the menu as two rounded rectangles, but as signed-distance fields - for any point, how far inside (negative) or outside (positive) the shape it is. Then fuse them with a smin (smooth minimum): where the two fields are close, it bridges them into one region with a gooey neck; where they're far apart, they stay separate. That single function gives you the merge and the pinch-off for free.
function smin(a, b, k) { // fuse two fields over a width k
const h = Math.max(k - Math.abs(a - b), 0) / k
return Math.min(a, b) - h * h * k * 0.25
}
const field = (x, y) =>
smin(sdRoundBox(x, y, button), sdRoundBox(x, y, menu), K)Trace the field into a shape()
Because the button sits above the menu, every horizontal row of the field is a single interval. So walk down in small steps; on each row find where field < 0 starts and ends; trace the right edge down, then the left edge back up, and close. A row with no interior ends the current sub-path - that's the gap that splits the blob into two when it settles, emitted as close, move to …, exactly like Aave's path.
for (let y = 0; y < H; y += 2) {
// scan the row → [xLeft, xRight] where field < 0
if (inside) band.push({ y, xLeft, xRight })
else band = null // gap → next sub-path
}
// each band → from … line to (right ↓) … line to (left ↑) … closeDrive one number
Everything is parameterised by openness t: the menu's bottom and left edges grow with it, and near the end its top lifts ~8px off the button so the field finally pinches. Animate that one scalar - a motion value on the easing curve - and map it to the live clip-path. Closed is just the button's outline (hidden under the real trigger), so it reads as the menu having lived inside the button.
const progress = useMotionValue(0)
const clipPath = useTransform(progress, blobShape)
useEffect(() => animate(progress, open ? 1 : 0, TRANSITION).stop, [open])Glass, labels, timing
The surface is frosted glass: a translucent fill plus backdrop-filter: blur(6px). The labels sit inside the clipped element, so the same clip-path masks them - they hold their final position and are uncovered as the blob arrives (Aave does this with a separate SVG mask; one clip is equivalent). And the feel is all in the curve: cubic-bezier(0.19, 1, 0.22, 1) over ~400ms, the same one driving the Copy link → Copied! text morph.
Is it worth it?
Honestly, for most UIs, no - this regenerates a many-segment path on every frame, and clip-path: shape() is Chromium/Safari-only (no Firefox yet). A pair of staggered rounded rectangles gets you 90% of the "liquid" read for a fraction of the cost. But if you want the genuine article - the neck that stretches and pinches into two distinct pills - a smooth-union field traced into a shape() is exactly how it's done.
Newsletter
Stay updated with my latest articles and projects. No spam, no nonsense.