Anatomy of Liquid Metaballs
How SVG circles, a classic goo filter, toroidal drift, and a cursor blob merge without React re-renders per frame.
↓Scroll to step through it
Circles, not meshes
LiquidMetaballs is physics on SVG circles under a blur + contrast filter.
Positions update with setAttribute so React stays out of the hot path:
the rAF loop never calls setState.
Defaults: blobCount={7}, colors cycling
#6366f1 / #a855f7 / #ec4899 / #06b6d4, radii roughly 8% to 16% of the short
side, velocity (rand - 0.5) * 0.6 * speed.
Edges wrap toroidally (leave left, enter right), so the field never piles up in a corner.
const paint = () => {
for (const b of blobsRef.current) {
b.el?.setAttribute("cx", `${b.x}`);
b.el?.setAttribute("cy", `${b.y}`);
}
};Writing attributes skips reconciliation. The circles are created once; the loop only mutates geometry.
The goo chain
The classic metaball filter: blur, then punch alpha back up with a contrast matrix:
Overlapping discs fuse into one liquid silhouette. Higher gooeyness =
softer merge.
Cursor merge
With interactive (default), an extra circle follows the pointer. While
active its radius is min(w,h) * 0.1; on leave it collapses to 0 so no
orphan disc remains:
It shares the same goo filter, so it melts into neighbors on contact.
The result
Move to merge. Leave and the cursor blob vanishes.
Physics runs on attributes and the fusion happens in a filter, so React never re-renders per frame.
- Blobs
- blobCount=7 · radius ~8 to 16% of min side
- Paint
- setAttribute cx/cy, no React re-render
- Wrap
- toroidal edges
b.r = min * (0.08 + Math.random() * 0.08);b.vx = (Math.random() - 0.5) * 0.6 * speed;b.vy = (Math.random() - 0.5) * 0.6 * speed;Anatomy of Liquid Metaballs
How SVG circles, a classic goo filter, toroidal drift, and a cursor blob merge without React re-renders per frame.
- Blobs
- blobCount=7 · radius ~8 to 16% of min side
- Paint
- setAttribute cx/cy, no React re-render
- Wrap
- toroidal edges
b.r = min * (0.08 + Math.random() * 0.08);b.vx = (Math.random() - 0.5) * 0.6 * speed;b.vy = (Math.random() - 0.5) * 0.6 * speed;Circles, not meshes
LiquidMetaballs is physics on SVG circles under a blur + contrast filter.
Positions update with setAttribute so React stays out of the hot path:
the rAF loop never calls setState.
Defaults: blobCount={7}, colors cycling
#6366f1 / #a855f7 / #ec4899 / #06b6d4, radii roughly 8% to 16% of the short
side, velocity (rand - 0.5) * 0.6 * speed.
Edges wrap toroidally (leave left, enter right), so the field never piles up in a corner.
const paint = () => {
for (const b of blobsRef.current) {
b.el?.setAttribute("cx", `${b.x}`);
b.el?.setAttribute("cy", `${b.y}`);
}
};Writing attributes skips reconciliation. The circles are created once; the loop only mutates geometry.
- Blur
- feGaussianBlur stdDeviation={gooeyness}
- Punch
- alpha ×20 − 9
<feGaussianBlur stdDeviation={gooeyness} /> {/* default 16 */}<feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" />The goo chain
The classic metaball filter: blur, then punch alpha back up with a contrast matrix:
Overlapping discs fuse into one liquid silhouette. Higher gooeyness =
softer merge.
- Active
- r = min(w,h)×0.1 while pointer in
- Idle
- r = 0, no leftover disc
cursorRef.current?.setAttribute("r", active ? `${min * 0.1}` : "0");cursorRef.current?.setAttribute("cx", `${cursor.x}`);cursorRef.current?.setAttribute("cy", `${cursor.y}`);Cursor merge
With interactive (default), an extra circle follows the pointer. While
active its radius is min(w,h) * 0.1; on leave it collapses to 0 so no
orphan disc remains:
It shares the same goo filter, so it melts into neighbors on contact.
The result
Move to merge. Leave and the cursor blob vanishes.
Physics runs on attributes and the fusion happens in a filter, so React never re-renders per frame.
Lifecycle + a11y
ResizeObserver, IntersectionObserver, and visibilitychange gate the
loop. prefers-reduced-motion stops motion (there is no still-frame bake; the last
positions freeze in place). Root is aria-hidden; the SVG is
role="presentation".
Motion Score
SVG + gooCircle positions through goo filter each frame