Skip to main content
The Squares component creates an animated grid background using HTML5 Canvas with customizable movement and hover interactions.

Overview

Squares provides:
  • Infinitely scrolling grid animation
  • Multiple direction modes (right, left, up, down, diagonal)
  • Interactive hover fill effects
  • Radial gradient vignette overlay
  • Customizable colors and dimensions

Props

direction
'right' | 'left' | 'up' | 'down' | 'diagonal'
default:"'right'"
Direction of grid movement
speed
number
default:"1"
Animation speed (pixels per frame)
borderColor
string
default:"'#999'"
Color of grid lines
squareSize
number
default:"40"
Size of each grid square in pixels
hoverFillColor
string
default:"'#222'"
Fill color when hovering over a square

Implementation

Squares.jsx
import { useRef, useEffect } from 'react';

const Squares = ({
  direction = 'right',
  speed = 1,
  borderColor = '#999',
  squareSize = 40,
  hoverFillColor = '#222'
}) => {
  const canvasRef = useRef(null);
  const requestRef = useRef(null);
  const numSquaresX = useRef(0);
  const numSquaresY = useRef(0);
  const gridOffset = useRef({ x: 0, y: 0 });
  const hoveredSquareRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');

    const resizeCanvas = () => {
      canvas.width = canvas.offsetWidth;
      canvas.height = canvas.offsetHeight;
      numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1;
      numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1;
    };

    window.addEventListener('resize', resizeCanvas);
    resizeCanvas();

    const drawGrid = () => {
      if (!ctx) return;
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
      const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;

      for (let x = startX; x < canvas.width + squareSize; x += squareSize) {
        for (let y = startY; y < canvas.height + squareSize; y += squareSize) {
          const squareX = x - (gridOffset.current.x % squareSize);
          const squareY = y - (gridOffset.current.y % squareSize);

          // Fill hovered square
          if (
            hoveredSquareRef.current &&
            Math.floor((x - startX) / squareSize) === hoveredSquareRef.current.x &&
            Math.floor((y - startY) / squareSize) === hoveredSquareRef.current.y
          ) {
            ctx.fillStyle = hoverFillColor;
            ctx.fillRect(squareX, squareY, squareSize, squareSize);
          }

          // Draw border
          ctx.strokeStyle = borderColor;
          ctx.strokeRect(squareX, squareY, squareSize, squareSize);
        }
      }

      // Apply radial gradient vignette
      const gradient = ctx.createRadialGradient(
        canvas.width / 2,
        canvas.height / 2,
        0,
        canvas.width / 2,
        canvas.height / 2,
        Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2
      );
      gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
      gradient.addColorStop(1, '#060010');

      ctx.fillStyle = gradient;
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    };

    const updateAnimation = () => {
      const effectiveSpeed = Math.max(speed, 0.1);
      
      switch (direction) {
        case 'right':
          gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
          break;
        case 'left':
          gridOffset.current.x = (gridOffset.current.x + effectiveSpeed + squareSize) % squareSize;
          break;
        case 'up':
          gridOffset.current.y = (gridOffset.current.y + effectiveSpeed + squareSize) % squareSize;
          break;
        case 'down':
          gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
          break;
        case 'diagonal':
          gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
          gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
          break;
      }

      drawGrid();
      requestRef.current = requestAnimationFrame(updateAnimation);
    };

    const handleMouseMove = event => {
      const rect = canvas.getBoundingClientRect();
      const mouseX = event.clientX - rect.left;
      const mouseY = event.clientY - rect.top;

      const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
      const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;

      const hoveredSquareX = Math.floor((mouseX + gridOffset.current.x - startX) / squareSize);
      const hoveredSquareY = Math.floor((mouseY + gridOffset.current.y - startY) / squareSize);

      if (
        !hoveredSquareRef.current ||
        hoveredSquareRef.current.x !== hoveredSquareX ||
        hoveredSquareRef.current.y !== hoveredSquareY
      ) {
        hoveredSquareRef.current = { x: hoveredSquareX, y: hoveredSquareY };
      }
    };

    const handleMouseLeave = () => {
      hoveredSquareRef.current = null;
    };

    canvas.addEventListener('mousemove', handleMouseMove);
    canvas.addEventListener('mouseleave', handleMouseLeave);
    requestRef.current = requestAnimationFrame(updateAnimation);

    return () => {
      window.removeEventListener('resize', resizeCanvas);
      if (requestRef.current) cancelAnimationFrame(requestRef.current);
      canvas.removeEventListener('mousemove', handleMouseMove);
      canvas.removeEventListener('mouseleave', handleMouseLeave);
    };
  }, [direction, speed, borderColor, hoverFillColor, squareSize]);

  return <canvas ref={canvasRef} className="w-full h-full border-none block" />;
};

export default Squares;

Usage Examples

Basic Usage

import Squares from './components/Squares/Squares';

function Background() {
  return (
    <div className="absolute inset-0">
      <Squares />
    </div>
  );
}

Custom Configuration

<Squares
  speed={0.5}
  squareSize={40}
  direction="diagonal"
  borderColor="#726e6e"
  hoverFillColor="#222"
/>

In Categoria Page

Categoria.jsx
import Squares from "../components/Squares/Squares";

function Categoria() {
  return (
    <div className="relative bg-[#0e0e0e] min-h-screen">
      <div className="absolute inset-0 z-0">
        <Squares 
          speed={0.3} 
          squareSize={30} 
          direction="diagonal" 
          borderColor="#333" 
          hoverFillColor="#222" 
        />
      </div>
      
      <div className="relative z-10">
        {/* Content */}
      </div>
    </div>
  );
}

Animation Mechanics

Infinite Scrolling

The grid uses modulo arithmetic for seamless infinite scrolling:
gridOffset.current.x = (gridOffset.current.x - speed + squareSize) % squareSize;
This creates the illusion of endless movement by resetting the offset when it reaches the square size.

Direction Modes

DirectionBehavior
rightGrid moves right to left
leftGrid moves left to right
upGrid moves bottom to top
downGrid moves top to bottom
diagonalGrid moves diagonally (right-down)

Hover Detection

Mouse position is converted to grid coordinates:
const hoveredSquareX = Math.floor(
  (mouseX + gridOffset.current.x - startX) / squareSize
);
const hoveredSquareY = Math.floor(
  (mouseY + gridOffset.current.y - startY) / squareSize
);
The hovered square is filled with the hoverFillColor before drawing borders.

Visual Effects

Radial Gradient Vignette

A radial gradient overlay creates depth:
const gradient = ctx.createRadialGradient(
  canvas.width / 2,
  canvas.height / 2,
  0,
  canvas.width / 2,
  canvas.height / 2,
  Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2
);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');  // Transparent center
gradient.addColorStop(1, '#060010');  // Dark purple edges

Responsive Canvas

The canvas automatically resizes to fill its container:
const resizeCanvas = () => {
  canvas.width = canvas.offsetWidth;
  canvas.height = canvas.offsetHeight;
};

window.addEventListener('resize', resizeCanvas);

Performance Considerations

  • Uses requestAnimationFrame for smooth 60fps animation
  • Efficient modulo-based scrolling avoids coordinate overflow
  • Only draws visible squares plus one extra row/column
  • Canvas is cleared and redrawn each frame
The component renders to an HTML5 Canvas element and fills its parent container. Ensure the parent has defined dimensions.
Use subtle colors for borderColor (low opacity grays) to create an elegant background pattern that doesn’t overpower content.

Build docs developers (and LLMs) love