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.
↓Scroll to step through it
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.
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.
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.
const ro = new ResizeObserver(measure);
for (const el of contentRefs.current) if (el) ro.observe(el);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.
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
{/* 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>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.
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
{/* 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>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.
- 1 · Apartnearness = 0, native only
- 2 · Neckingnearness peaks, goo on
- 3 · Mergednearness = 0 again, native
- 4 · Necking outgoo flashes on the way back
nearness peaks mid-gap and is zero when fully apart or fully merged
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));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.
raw silhouettes
after goo filter
- 1 · Apartgap open, no overlap
- 2 · Approachhalos start to kiss
- 3 · Fusedcolor-matrix seals the bridge
- 4 · Separatebridge thins, then snaps
<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"/>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.
const ro = new ResizeObserver(measure);
for (const el of contentRefs.current) if (el) ro.observe(el);MCP Connector
GitHub
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
transformCard stack merge / expandSVG goo filterGoo merge + animated blur