// FREE TOOLSView all →
◈Keyword ResearchFreeReal Google + Bing suggestions, grouped by intent.◆Image MakerNewBlog thumbnails, OG images & social cards in seconds.
▣Meta & SERP PreviewsoonSee how your page looks in Google, fix title & description.
◐AI Visibility ChecksoonWhat AI assistants say about your brand.
❖Schema GeneratorsoonCopy-paste JSON-LD for FAQ, Article, Product & more.
← 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.
// 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 →