Grid Pattern

A customizable animated grid background component for creating dynamic patterned layouts behind content.

1. Copy the component file

Create a new file called grid-pattern-background.jsx in your reusable components folder (for example /src/components/) and paste the following code into it.

grid-pattern-background.jsx

import { memo, useCallback, useLayoutEffect, useEffect, useRef, useMemo, useState } from "react";
import styles from "./grid-pattern-background.module.css";

const GridPatternBackground = (props) => {
  const {
    children,
    className = "",
    backgroundColor = "rgba(0, 0, 0, 1)", 
    gridColor = "rgba(255, 255, 255, 0.5)",
    gridSize = 25,
    direction = "down",
    speed = 1,
    wrapperProps = {},
    wrapperTagName = "div",
    style = null,
    ...restProps
  } = props;

  const {
    className: wrapperClassName = "",
    ...restWrapperProps
  } = wrapperProps;

  const Wrapper = wrapperTagName || "div";

  const containerRef = useRef(null);
  const canvasRef = useRef(null);
  const rafId = useRef(null);
  const translate = useRef({ x: 0, y: 0 });

  const [mounted, setMounted] = useState(false);
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);

  const { devicePixelRatio, canvasWidth, canvasHeight } = useMemo(() => {
    const devicePixelRatio = globalThis.devicePixelRatio || 1;
    return {
      devicePixelRatio,
      canvasWidth: width * devicePixelRatio,
      canvasHeight: height * devicePixelRatio,
    };
  }, [width, height]);

  const ctx = useMemo(() => {
    return canvasRef.current?.getContext("2d");
  }, [canvasRef.current]);

  const _size = Math.min(Math.max(10, gridSize), 100);
  const _speed = Math.min(Math.max(0, speed), 5);

  const render = useCallback(() => {
    switch(direction) {
      case "up":
        translate.current.y -= _speed;    
        break;
      case "down":
        translate.current.y += _speed;    
        break;
      case "left":
        translate.current.x -= _speed;    
        break;
      case "right":
        translate.current.x += _speed;    
        break;
      case "top-left":
        translate.current.x -= _speed;    
        translate.current.y -= _speed;
        break;
      case "top-right":
        translate.current.x += _speed;    
        translate.current.y -= _speed;
        break;
      case "bottom-left":
        translate.current.x -= _speed;    
        translate.current.y += _speed;
        break;
      case "bottom-right":
        translate.current.x += _speed;    
        translate.current.y += _speed;
        break;
    }
    translate.current.x %= _size;
    translate.current.y %= _size;
    ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
    for (let i = -_size; i <= (height + _size); i += _size) {
      ctx.beginPath();
      ctx.moveTo(0, i + translate.current.y);
      ctx.lineTo(width, i + translate.current.y);
      ctx.strokeStyle = gridColor;
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.closePath();
    }
    for (let i = -_size; i <= (width + _size); i += _size) {
      ctx.beginPath();
      ctx.moveTo(i + translate.current.x, 0);
      ctx.lineTo(i + translate.current.x, height);
      ctx.strokeStyle = gridColor;
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.closePath();
    }
    if(_speed !== 0) {
      rafId.current = requestAnimationFrame(render);
    }
  }, [
    ctx,
    devicePixelRatio,
    width,
    height,
    canvasWidth,
    canvasHeight,
    gridColor,
    direction,
    _size,
    _speed
  ]);

  useEffect(() => {
    const updateContainerDimensions = () => {
      const {
        width,
        height,
      } = containerRef.current.getBoundingClientRect();
      setWidth(width);
      setHeight(height);
    };
    const resizeObserver = new ResizeObserver(updateContainerDimensions);
    resizeObserver.observe(containerRef.current);
    updateContainerDimensions();
    setMounted(true);
    return () => {
      resizeObserver.disconnect();
    };
  }, []);

  useLayoutEffect(() => {
    if (!mounted) return;
    render();
    return () => {
      cancelAnimationFrame(rafId.current);
    };
  }, [mounted, render]);

  return (
    <div
      {...restProps}
      ref={containerRef}
      className={[
        className,
        styles["grid-pattern-background"],
      ].join(" ")}
      style={{
        ...style,
        "--background-color": backgroundColor,
      }}
    >
      <canvas
        aria-hidden={true}
        width={canvasWidth}
        height={canvasHeight}
        ref={canvasRef}
      />
      <Wrapper
        {...restWrapperProps}
        className={[
          wrapperClassName,
          styles["wrapper"],
        ].join(" ")}
      >
        {children}
      </Wrapper>
    </div>
  );
};

export default memo(GridPatternBackground);

2. Copy the CSS module file

In the same folder, create a file called grid-pattern-background.module.css and paste the following CSS.

grid-pattern-background.module.css

.grid-pattern-background {
  position: relative;
  background-color: var(--background-color);

  >canvas {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
  }

  >.wrapper {
    position: relative;
    z-index: 2;
  }
}

3. Use the component

Now you can import and use the component anywhere in your project.

grid-pattern-background-preview.jsx

import GridPatternBackground from "@/components/backgrounds/grid-pattern-background/grid-pattern-background";

const GridPatternBackgroundPreview = () => {
  return (
    <GridPatternBackground
      backgroundColor="#040c1a"
      gridColor="#3c4d55"
      style={{
        width: "100%",
        height: "100%",
      }}
      wrapperProps = {{
        style: {
          display: "grid",
          placeItems: "center",
          height: "100%"
        }
      }}
    >
      <h2 
        style={{
        color: "#fff"
        }}
      >
        Grid Pattern Background
      </h2>
    </GridPatternBackground>
  );
};

export default GridPatternBackgroundPreview;

Direction

grid-pattern-background

import { useState } from "react";
import GridPatternBackground from "@/components/backgrounds/grid-pattern-background/grid-pattern-background";

const GridPatternBackgroundDirectionPreview = () => {
  const directions = ["up", "down", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right"];

  const [direction, setDirection] = useState("down");

  return (
    <GridPatternBackground
      direction={direction}
      backgroundColor="#040c1a"
      gridColor="#3c4d55"
      style={gridBackgroundStyles}
      wrapperProps = {{
        style: wrapperStyles,
      }}
    >
      {directions.map(dir => (
        <button
          key={dir}
          onClick={() => setDirection(dir)}
          style={{
            ...buttomStyles,
            ...(dir === direction ? activeButtonStyles : null),
          }}
        >
          {dir}
        </button>
      ))}
    </GridPatternBackground>
  );
};

const gridBackgroundStyles = {
  width: "100%",
  height: "100%",
  padding: "24px",
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
};
const wrapperStyles = {
  display: "flex",
  flexWrap: "wrap",
  justifyContent: "center",
  alignItems: "center",
  gap: "16px",
};
const buttomStyles = {
  padding: "8px 16px",
  border: "1px solid #fff",
  borderRadius: "4px",
  background: "transparent",
  font: "inherit",
  color: "#fff",
  textTransform: "uppercase",
};
const activeButtonStyles = {
  background: "#fff",
  color: "#111",
};

export default GridPatternBackgroundDirectionPreview;

Speed

Speed: 1

grid-pattern-background

import { useState } from "react";
import GridPatternBackground from "@/components/backgrounds/grid-pattern-background/grid-pattern-background";

const GridPatternBackgroundSpeedPreview = () => {
  const [speed, setSpeed] = useState(1);

  const handleSpeedChange = (e) => {
    setSpeed(parseFloat(e.target.value));
  };

  return (
    <GridPatternBackground
      speed={speed}
      backgroundColor="#040c1a"
      gridColor="#3c4d55"
      style={gridBackgroundStyles}
      wrapperProps = {{
        style: wrapperStyles,
      }}
    >
      <span style={labelStyles}>
        Speed: {speed}
      </span>
      <input
        type="range"
        min="0"
        max="5"
        step="0.1"
        value={speed}
        onChange={handleSpeedChange}
      />
    </GridPatternBackground>
  );
};

const gridBackgroundStyles = {
  width: "100%",
  height: "100%",
  display: "grid",
  placeItems: "center",
};
const wrapperStyles = {
  display: "grid",
  placeItems: "center",
  gap: "8px",
};
const labelStyles = {
  color: "#fff",
};

export default GridPatternBackgroundSpeedPreview;

Grid Size

Grid Size: 50

grid-pattern-background

import { useState } from "react";
import GridPatternBackground from "@/components/backgrounds/grid-pattern-background/grid-pattern-background";

const GridPatternBackgroundGridSizePreview = () => {
  const [gridSize, setGridSize] = useState(50);

  const handleSpeedChange = (e) => {
    setGridSize(parseInt(e.target.value));
  };

  return (
    <GridPatternBackground
      gridSize={gridSize}
      backgroundColor="#040c1a"
      gridColor="#3c4d55"
      style={gridBackgroundStyles}
      wrapperProps = {{
        style: wrapperStyles,
      }}
    >
      <span style={labelStyles}>
        Grid Size: {gridSize}
      </span>
      <input
        type="range"
        min="10"
        max="100"
        step="1"
        value={gridSize}
        onChange={handleSpeedChange}
      />
    </GridPatternBackground>
  );
};

const gridBackgroundStyles = {
  width: "100%",
  height: "100%",
  display: "grid",
  placeItems: "center",
};
const wrapperStyles = {
  display: "grid",
  placeItems: "center",
  gap: "8px",
};
const labelStyles = {
  color: "#fff",
};

export default GridPatternBackgroundGridSizePreview;

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
childrenReactNodeYesContent rendered inside the grid background wrapper.
classNamestringNoAdditional CSS classes applied to the root container.
styleReact.CSSPropertiesNonullInline styles applied to the root container.
backgroundColorstringNo"rgba(0, 0, 0, 1)"Background color behind the grid pattern. Accepts any valid CSS color.
gridColorstringNo"rgba(255, 255, 255, 0.5)"Color of the grid lines. Accepts any valid CSS color value.
gridSizenumberNo25Size of each grid cell in pixels. Range: 10 – 100.
direction"up" | "down" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right"No"down"Direction in which the grid animation moves.
speednumberNo1Animation speed multiplier. Range: 0 – 5. 0 disables movement.
wrapperTagNamestringNo"div"HTML tag used as the wrapper element around the children.
wrapperPropsHTMLAttributes<HTMLElement>No{}Additional props passed to the wrapper element containing the children.