← all components
#02

Gooey liquid cursor comet

A gooey liquid comet that elastically chases your cursor, a chain of blobs merged into one liquid mass with a blur+contrast filter and a glowing gradient tail. Canvas-based, 60fps.

ReactCanvasJS
// LIVE DEMO

Liquid follows you

A gooey comet that chases your cursor with elastic lag.

share this component →FacebookX / TwitterLinkedIn
// THE CODE
React
"use client";
import { useEffect, useRef } from "react";

export default function LiquidComet() {
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current, ctx = cv.getContext("2d");
    if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let w, h, raf, t = 0;
    const DPR = Math.min(devicePixelRatio || 1, 2);
    const target = { x: 0, y: 0 };
    const blobs = Array.from({ length: 9 }, (_, i) => ({
      x: 0, y: 0, r: 58 - i*4.5, lag: 0.22 - i*0.02, hue: 190 + i*9,
    }));

    function resize() {
      const r = cv.getBoundingClientRect(); w = r.width; h = r.height;
      cv.width = w*DPR; cv.height = h*DPR; ctx.setTransform(DPR,0,0,DPR,0,0);
      if (target.x === 0) { target.x = w/2; target.y = h/2; blobs.forEach(b => { b.x = w/2; b.y = h/2; }); }
    }
    function frame() {
      t += .016;
      ctx.fillStyle = "rgba(6,9,18,.22)"; ctx.fillRect(0,0,w,h);
      blobs[0].x += (target.x - blobs[0].x)*blobs[0].lag;
      blobs[0].y += (target.y - blobs[0].y)*blobs[0].lag;
      for (let i = 1; i < blobs.length; i++) {
        blobs[i].x += (blobs[i-1].x - blobs[i].x)*(blobs[i].lag + .12);
        blobs[i].y += (blobs[i-1].y - blobs[i].y)*(blobs[i].lag + .12);
      }
      ctx.save(); ctx.filter = "blur(12px) contrast(26)"; ctx.globalCompositeOperation = "lighter";
      for (let i = 0; i < blobs.length; i++) {
        const b = blobs[i], wob = 1 + Math.sin(t*2.4 + i*.7)*.18;
        const g = ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,b.r*wob);
        g.addColorStop(0,`hsl(${b.hue},95%,62%)`); g.addColorStop(1,`hsl(${b.hue},95%,50%)`);
        ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x,b.y,b.r*wob,0,7); ctx.fill();
      }
      ctx.restore(); raf = requestAnimationFrame(frame);
    }
    const onMove = e => { const r = cv.getBoundingClientRect(); target.x = e.clientX - r.left; target.y = e.clientY - r.top; };
    resize(); frame();
    addEventListener("resize", resize); cv.addEventListener("pointermove", onMove);
    return () => { cancelAnimationFrame(raf); removeEventListener("resize", resize); cv.removeEventListener("pointermove", onMove); };
  }, []);
  return <canvas ref={ref} className="w-full h-[360px] rounded-2xl bg-[#060912]" />;
}
// HOW IT WORKS
  • 01A chain of circles follows the cursor, each one chasing the blob ahead of it with a slightly different lag, so it stretches into a comet tail.
  • 02A blur + high-contrast canvas filter fuses the overlapping circles into a single smooth liquid shape (the metaball trick).
  • 03Additive blend and a translucent fill each frame add glow and a soft motion trail. Respects prefers-reduced-motion.
Want a custom component for your product?
Let's build it →