Grid Dot Background

Dot grid where dots near the cursor glow and enlarge like a radar pulse

backgroundgriddotsinteractivecursor
Preview

Source Code

"use client";

import { useRef, useEffect } from "react";

interface GridDotBackgroundProps {
  children?: React.ReactNode;
  dotColor?: string;
  spacing?: number;
  glowRadius?: number;
  className?: string;
}

export default function GridDotBackground({
  children,
  dotColor = "#38bdf8",
  spacing = 30,
  glowRadius = 120,
  className = "",
}: GridDotBackgroundProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const mouseRef = useRef({ x: -1000, y: -1000 });
  const animRef = useRef(0);

  useEffect(() => {
    const canvas = canvasRef.current;
    const container = containerRef.current;
    if (!canvas || !container) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    let cssW = 0, cssH = 0;

    const resize = () => {
      const dpr = window.devicePixelRatio || 1;
      cssW = container.clientWidth;
      cssH = container.clientHeight;
      canvas.width = cssW * dpr;
      canvas.height = cssH * dpr;
      canvas.style.width = `${cssW}px`;
      canvas.style.height = `${cssH}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };

    resize();
    window.addEventListener("resize", resize);

    const handleMouseMove = (e: MouseEvent) => {
      const rect = container.getBoundingClientRect();
      mouseRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    };
    const handleMouseLeave = () => {
      mouseRef.current = { x: -1000, y: -1000 };
    };

    container.addEventListener("mousemove", handleMouseMove);
    container.addEventListener("mouseleave", handleMouseLeave);

    // Parse color to RGB
    const tempEl = document.createElement("div");
    tempEl.style.color = dotColor;
    document.body.appendChild(tempEl);
    const computed = getComputedStyle(tempEl).color;
    document.body.removeChild(tempEl);
    const rgb = computed.match(/\d+/g)?.map(Number) || [56, 189, 248];

    const animate = () => {
      ctx.clearRect(0, 0, cssW, cssH);
      const mouse = mouseRef.current;

      for (let x = spacing; x < cssW; x += spacing) {
        for (let y = spacing; y < cssH; y += spacing) {
          const dx = x - mouse.x;
          const dy = y - mouse.y;
          const dist = Math.sqrt(dx * dx + dy * dy);
          const influence = Math.max(0, 1 - dist / glowRadius);

          const baseSize = 1.5;
          const size = baseSize + influence * 4;
          const alpha = 0.15 + influence * 0.8;

          ctx.beginPath();
          ctx.arc(x, y, size, 0, Math.PI * 2);
          ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`;
          ctx.fill();

          // Glow for nearby dots
          if (influence > 0.2) {
            ctx.beginPath();
            ctx.arc(x, y, size * 3, 0, Math.PI * 2);
            ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${influence * 0.1})`;
            ctx.fill();
          }
        }
      }

      animRef.current = requestAnimationFrame(animate);
    };

    animate();

    return () => {
      cancelAnimationFrame(animRef.current);
      window.removeEventListener("resize", resize);
      container.removeEventListener("mousemove", handleMouseMove);
      container.removeEventListener("mouseleave", handleMouseLeave);
    };
  }, [dotColor, spacing, glowRadius]);

  return (
    <div ref={containerRef} className={`relative overflow-hidden bg-[#050510] ${className}`}>
      <canvas ref={canvasRef} className="absolute inset-0" />
      <div className="pointer-events-none relative z-10">{children}</div>
    </div>
  );
}