GodUIGodUI
85

Anatomy of Liquid Metaballs

How SVG circles, a classic goo filter, toroidal drift, and a cursor blob merge without React re-renders per frame.

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.

Circles, not meshes

Defaults: blobCount={7}, colors cycling #6366f1 / #a855f7 / #ec4899 / #06b6d4, radii roughly 8–16% of the short side, velocity (rand - 0.5) * 0.6 * speed.

tsx
 
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;

Edges wrap toroidally — leave left, enter right — so the field never piles up in a corner.

AnatomySVG circles · goo filter
Blobs
blobCount=7 · radius ~8–16% of min side
Paint
setAttribute cx/cy — no React re-render
Wrap
toroidal edges

Why setAttribute, not React

tsx
 
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

Classic metaball filter — blur, then punch alpha back up with a contrast matrix:

tsx
 
<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" />

Overlapping discs fuse into one liquid silhouette. Higher gooeyness = softer merge.

Goo filterblur + contrast matrix
raw
gooeyness=16
Blur
feGaussianBlur stdDeviation={gooeyness}
Punch
alpha ×20 − 9

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:

tsx
 
cursorRef.current?.setAttribute("r", active ? `${min * 0.1}` : "0");
cursorRef.current?.setAttribute("cx", `${cursor.x}`);
cursorRef.current?.setAttribute("cy", `${cursor.y}`);

It shares the same goo filter, so it melts into neighbors on contact.

Cursor blobr grows on pointer
Active
r = min(w,h)×0.1 while pointer in
Idle
r = 0 — no leftover disc

Lifecycle + a11y

ResizeObserver, IntersectionObserver, and visibilitychange gate the loop. prefers-reduced-motion stops motion (no still-frame bake — the last positions simply freeze). Root is aria-hidden; the SVG is role="presentation".

The result

Move to merge. Leave and the cursor blob vanishes.

Resultthe real component — move to merge

Physics on attributes, fusion in a filter, zero React re-renders per frame.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub