Workspace / component / shaders

Metaballs Shader

The Pax bubble engine melts blobs together over your content with surface tension thresholds. Blob count and speed are props.

Workspacecomponentshaders

paxfx add blocks/component-shaders-metaballslocal only

Tier: high. Reduced motion: Blobs rest in a fixed composition.

"use client";

import type { ReactNode } from "react";
import { cn, EffectSurface } from "@pax-sh/blocks";

export interface MetaballsShaderProps {
  className?: string;
  /** Content the blobs melt across. Defaults to a demo panel. */
  children?: ReactNode;
}

function DemoContent() {
  return (
    <div className="flex min-h-[320px] flex-col justify-center gap-5 p-8 md:p-12">
      <p className="m-0 text-xs font-medium uppercase tracking-[0.22em] text-[var(--pax-acid,#c8ff00)]">
        Surface tension / field render
      </p>
      <h3 className="m-0 max-w-xl text-3xl font-semibold leading-tight tracking-tight [text-wrap:balance] md:text-4xl">
        Blobs that actually merge.
      </h3>
      <p className="m-0 max-w-md text-sm leading-relaxed text-[var(--pax-paper,#f6f2ea)] opacity-70">
        The bubble engine evaluates a scalar field and thresholds it, so two blobs near each other
        neck together the way real fluids do instead of just overlapping.
      </p>
      <div className="mt-2 flex flex-wrap gap-2">
        {["Threshold field", "Organic motion", "Runs over HTML"].map((label) => (
          <span
            key={label}
            className="rounded-full border px-3 py-1 text-[11px] font-medium uppercase tracking-[0.14em] text-[var(--pax-paper,#f6f2ea)]"
            style={{ borderColor: "color-mix(in srgb, var(--pax-paper,#f6f2ea) 22%, transparent)" }}
          >
            {label}
          </span>
        ))}
      </div>
    </div>
  );
}

/**
 * Mounts the Pax bubble engine over live HTML: metaballs drift across the
 * surface and melt together at surface-tension thresholds. Under reduced
 * motion the blobs rest in a fixed composition.
 */
export function MetaballsShader({ className, children }: MetaballsShaderProps) {
  return (
    <div
      className={cn(
        "relative isolate overflow-hidden rounded-2xl border border-[var(--pax-border,rgba(18,17,15,0.12))]",
        "bg-[var(--pax-night,#0b0d10)] text-[var(--pax-paper,#f6f2ea)]",
        className,
      )}
      data-pax-asset="component-shaders-metaballs"
    >
      <EffectSurface effect="bubble" tier="high" className="h-full w-full">
        {children ?? <DemoContent />}
      </EffectSurface>
    </div>
  );
}