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
Animation speed (pixels per frame)
Size of each grid square in pixels
Fill color when hovering over a square
Implementation
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
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
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
| Direction | Behavior |
|---|
right | Grid moves right to left |
left | Grid moves left to right |
up | Grid moves bottom to top |
down | Grid moves top to bottom |
diagonal | Grid 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);
- 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.