← all components
#11

Ripple dot grid

A calm grid of dots that stays alive with a slow ambient wave, then ripples outward from your cursor: nearby dots swell, brighten, and lift as a wave passes through them. Each dot is animated from its distance to the pointer, drawn on canvas.

ReactCanvasJSHTML
// LIVE DEMO

Ripple grid

Move your cursor. The grid ripples away from it.

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

export default function RippleGrid() {
  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), GAP = 26;
    const m = { x: -9999, y: -9999, on: false };
    let cols, rows, ox, oy;
    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);
      cols = Math.floor(w/GAP); rows = Math.floor(h/GAP);
      ox = (w - (cols-1)*GAP)/2; oy = (h - (rows-1)*GAP)/2;
    }
    function frame() {
      t += .03; ctx.clearRect(0,0,w,h);
      for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
        const x = ox + i*GAP, y = oy + j*GAP;
        const amb = Math.sin(i*.5 + j*.4 + t)*.5 + .5;
        let b = 0;
        if (m.on) {
          const d = Math.hypot(x - m.x, y - m.y), wave = Math.sin(d*.06 - t*3);
          b = Math.max(0, 1 - d/160) * (.5 + wave*.5);
        }
        const r = 1.1 * (.6 + amb*.6 + b*2.4);
        ctx.fillStyle = `hsla(${200 + b*60},95%,${60 + b*15}%,${.18 + amb*.15 + b*.7})`;
        ctx.beginPath(); ctx.arc(x, y - b*6, Math.max(.4, r), 0, Math.PI*2); ctx.fill();
      }
      raf = requestAnimationFrame(frame);
    }
    function onMove(e){ const r = cv.getBoundingClientRect(); m.x = e.clientX-r.left; m.y = e.clientY-r.top; m.on = true; }
    function onLeave(){ m.on = false; m.x = m.y = -9999; }
    resize(); frame();
    addEventListener("resize", resize); cv.addEventListener("pointermove", onMove); cv.addEventListener("pointerleave", onLeave);
    return () => { cancelAnimationFrame(raf); removeEventListener("resize", resize);
      cv.removeEventListener("pointermove", onMove); cv.removeEventListener("pointerleave", onLeave); };
  }, []);
  return <canvas ref={ref} className="w-full h-[380px] rounded-2xl bg-[#05080f]" />;
}
// HOW IT WORKS
  • 01Dots are laid out on a fixed grid and centered in the box, so the pattern is even at any size.
  • 02Every dot has an ambient sine wave based on its grid position, so the grid gently breathes even without the cursor.
  • 03Near the cursor, a distance-based sine adds a ripple: dots scale up, shift hue, brighten, and lift on the Y axis.
  • 04It clears and redraws each frame in one loop. Respects prefers-reduced-motion.
Want a custom component for your product?
Let's build it →