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 ↑) … close

Drive 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.