Grid Pattern

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

Usage

grid-pattern-background-preview.tsx
import GridPatternBackground from "@/registry/backgrounds/grid-pattern-background/grid-pattern-background";

const GridPatternBackgroundPreview = () => {
  return (
    <div className="relative w-full h-full grid place-items-center">
      <GridPatternBackground 
        direction="top-left"
        size={25}
      />
      <HeroSection />
    </div>
  );
};

const HeroSection = () => (
  <section className="relative z-10 flex items-center justify-center p-8">
    <div className="mx-auto max-w-md text-center">
      <div className="mb-5 lg:mb-6 inline-flex items-center leading-0 rounded-full border border-gray-300 dark:border-white/15 px-4 py-4 text-sm font-medium text-gray-800 dark:text-white backdrop-blur-md">
        ⚛️ Background Component
      </div>
      <h1 className="text-4xl font-bold tracking-tight text-gray-800 dark:text-white lg:text-5xl text-balance">
        Cartesian Coordinates Never Miss Meetings
      </h1>
      <p className="mx-auto mt-6 max-w-md text-md lg:text-lg leading-6 text-gray-500 dark:text-white/70 text-balance">
        Every intersection has a purpose. Every pixel has accountability. Revolutionary, really.
      </p>
      <div className="mt-6 lg:mt-8">
        <button className="rounded-full border border-gray-300 dark:border-white/20 border-gray-50 bg-white/10 px-8 py-3 text-sm font-semibold text-gray-800 dark:text-white backdrop-blur-md transition-all duration-300 hover:bg-gray-100/50 dark:hover:bg-white/20">
          Get Component
        </button>
      </div>
    </div>
  </section>
);

export default GridPatternBackgroundPreview;

Component

Install the following packages before using this component.

Tailwind CSS
grid-pattern-background.tsx
import { memo, useCallback, useLayoutEffect, useEffect, useRef, useMemo, useState, type ComponentProps } from "react";
import cn from "@/utils/cn";

export type GridPatternBackgroundProps = {
  strokeColor?: string;
  size?: number;
  speed?: number;
  direction?: (
    "top" | 
    "right" | 
    "bottom" | 
    "left" | 
    "top-left" | 
    "top-right" | 
    "bottom-left" | 
    "bottom-right"
  );
} & ComponentProps<"div">;

const GridPatternBackground = (
  props: GridPatternBackgroundProps,
) => {
  const {
    strokeColor = "rgba(127, 127, 127, 0.5)",
    size = 10,
    direction = "top",
    speed = 1,
    className = "",
    ...restProps
  } = props;

  const containerRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const rafId = useRef<ReturnType<typeof requestAnimationFrame>>(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 = Math.max(1, 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, size), 100);
  const _speed = Math.min(Math.max(0, speed), 5);

  const render = useCallback(() => {
    if (!ctx) return;
    switch(direction) {
      case "top":
        translate.current.y -= _speed;    
        break;
      case "bottom":
        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 = strokeColor;
      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 = strokeColor;
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.closePath();
    }
    if(_speed !== 0) {
      rafId.current = requestAnimationFrame(render);
    }
  }, [
    ctx,
    devicePixelRatio,
    width,
    height,
    canvasWidth,
    canvasHeight,
    strokeColor,
    direction,
    _size,
    _speed
  ]);

  useEffect(() => {
    if (!containerRef.current) return;
    const updateContainerDimensions = () => {
      if (!containerRef.current) return;
      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 () => {
      if (rafId.current) {
        cancelAnimationFrame(rafId.current);
      }
    };
  }, [mounted, render]);

  return (
    <div
      {...restProps}
      ref={containerRef}
      className={cn("absolute top-[0] left-[0] right-[0] bottom-[0] overflow-hidden", className)}
    >
      <canvas
        aria-hidden={true}
        width={canvasWidth}
        height={canvasHeight}
        ref={canvasRef}
        className="w-full h-full"
      />
    </div>
  );
};

export default memo(GridPatternBackground);

Component API

PropTypeRequiredDefaultDescription
classNamestringNoAdditional CSS classes applied to the root container.
sizenumberNo10Size of each grid cell in pixels. Range: 10 – 100.
speednumberNo1Animation speed multiplier. Range: 0 – 5. 0 disables movement.
strokeColorstringNo"rgba(127, 127, 127, 0.5)"Color of the grid lines. Accepts any valid CSS color value.
direction"top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right"No"top"Direction in which the grid animation moves.

Support the Project

If you find this component useful, consider starring the repository on GitHub. Found a bug or have a suggestion? Open an issue to help improve it.