GodUIGodUI
129Follow on X

Anatomy of Gooey Stack

How stacked cards fuse into a liquid metaball bridge with three layers, one SVG filter, and a band-pass that turns the goo on only while surfaces are close.

01/04Three layers, same two cards

merge surface

native surface

together

Merge surface
blurred silhouettes, fused by the goo filter
Native surface
crisp bg-card + border, pixel-perfect at rest
Content
your children, never filtered or faded flat
tsx
{/* Merge surface — full-size silhouettes fused under the goo filter */}<motion.div style={{ opacity: gooOpacity, filter: `url(#${filterId})` }}>{items.map((_, i) => <motion.div className="absolute bg-card" ... />)}</motion.div>{/* Native surface — real DOM cards with CSS borders */}<motion.div style={{ opacity: nativeOpacity }}>{items.map((_, i) => <motion.div className="absolute border border-border bg-card" ... />)}</motion.div>
01

Three layers, same two cards

Gooey Stack never bends a border-radius or animates a border-radius or a clip-path. The liquid look is an SVG filter fusing rectangles, cross-faded against the plain crisp cards underneath, and only while it needs to be.

Every card position is drawn three times. A merge surface (blurred silhouettes, behind everything) gets pushed through the goo filter so close cards visually fuse. A native surface (real bg-card rects with CSS borders) sits on top of it, pixel-perfect at every zoom. Your content sits on top of that, unfiltered and always sharp. It recedes and frosts with the cards, but text never blurs.

02/04Nearness: a band-pass, not a threshold
  1. 1 · Apartnearness = 0, native only
  2. 2 · Neckingnearness peaks, goo on
  3. 3 · Mergednearness = 0 again, native
  4. 4 · Necking outgoo flashes on the way back

nearness peaks mid-gap and is zero when fully apart or fully merged

tsx
const nearnessAt = (g, expandedGap, collapsedGap) =>clamp((expandedGap - g) / Math.max(1, expandedGap - 4), 0, 1) *clamp((g - collapsedGap) / 20, 0, 1);const nearness = useTransform(gapSpring, (live) => nearnessAt(live, expandedGap, collapsedGap));const gooOpacity = useTransform(nearness, (v) => (reduce ? 0 : v));const nativeOpacity = useTransform(nearness, (v) => (reduce ? 1 : 1 - v));
02

Nearness: a band-pass, not a threshold

The goo shouldn't show at rest, either fanned out or fully merged (a receded card should leave no ghost behind the anchor). nearness is a band-pass on the live gap: zero at both ends, peaking only while surfaces are necking. The slow loop below holds four steps so the cross-fade is easy to see: apart → necking (goo on) → merged (goo off) → necking out.

It reads off a spring following the live gap instead of the target gap. At both rest ends the target's nearness is already 0, so deriving the cross-fade from the target alone would mean the goo never appears mid-toggle at all.

03/04The goo filter

raw silhouettes

after goo filter

  1. 1 · Apartgap open, no overlap
  2. 2 · Approachhalos start to kiss
  3. 3 · Fusedcolor-matrix seals the bridge
  4. 4 · Separatebridge thins, then snaps
tsx
<feGaussianBlur in="SourceGraphic" stdDeviation={gooeyness} result="blur" /><feColorMatrixin="blur"mode="matrix"values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 80 -40"result="goo"/>
03

The goo filter

Two SVG primitives do the fusing: feGaussianBlur softens the source into overlapping halos, then feColorMatrix multiplies the alpha channel back up past 1 and clips it. That steep threshold turns "soft overlapping blur" into "one hard-edged fused shape," almost without anti-aliasing. The demo crawls through the same four steps so you can see the bridge form and tear apart.

The real filter also regrows a thin, radially-symmetric border ring around the fused shape from a second small blur + threshold pass. A Gaussian stays round at corners and through the neck, where a box-shadow or clip-path approach would go square or wobble with curvature.

Nothing here ever animates border-radius, height, or clip-path. Cards move on y/scale/opacity, and blur is a static filter on a layer that itself only fades in and out. Every frame is compositor work, whether there are two cards or a dozen stacked behind the anchor.

A ResizeObserver per card keeps the stack's height and each card's bottom offset matched to its real content, so the layout survives text reflow or a resize without measuring on every animation frame. The observer fires only on size changes, never on spring ticks.

tsx
 
const ro = new ResizeObserver(measure);
for (const el of contentRefs.current) if (el) ro.observe(el);
04/04Result

MCP Connector

GitHub

04

The result

All the pieces together. Tap the plug and the prompt card merges in, then splits back out.

The liquid effect is two silhouette layers, one band-pass, and a filter that only touches pixels that are close.

Accessibility

The stack is presentational. Your own button, switch, or slider drives collapsed or gap, and the surface layers are aria-hidden so only your content is read. Under prefers-reduced-motion, the goo filter is dropped entirely and cards snap directly between states instead of springing through the neck.

Motion Score

Gooey StackCC: Paint-triggering
StransformCard stack merge / expand
CSVG goo filterGoo merge + animated blur
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →