Classic Dots Pattern

Flexible dotted background component with configurable color, spacing, scale, and multiple pattern variants, designed to wrap and enhance UI sections with subtle visual texture.

1. Copy the component file

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

classic-dots-pattern-background.jsx

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

const ClassicDotsPatternBackground = (props) => {
  const {
    children,
    variant = "standard",
    dotColor = "rgb(255, 255, 255)",
    dotScale = 1,
    gap = 4,
    // radial variant configuration props
    radialDirection = "in",
    radialScale = 0.25,
    // random variant configuration prop
    density = 0.5,
    // generic props
    className = "",
    wrapperTagName = "div",
    wrapperProps = {},
    ...restProps
  } = props;

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

  const Wrapper = wrapperTagName || "div";

  const containerRef = useRef(null);
  const canvasRef = useRef(null);

  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 _dotColor = useMemo(() => {
    const canvas = document.createElement("canvas");
    canvas.width = canvas.height = 1;
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = dotColor;
    ctx.fillRect(0, 0, 1, 1);
    const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
    canvas.remove();
    return (alpha) => {
      return `rgba(${r}, ${g}, ${b}, ${alpha})`;
    };
  }, [dotColor]);

  const _dotScale = useMemo(() => (
    Math.min(5, Math.max(0.1, dotScale))
  ), [dotScale]);

  const _gap = useMemo(() => (
    Math.max(1, gap)
  ), [gap]);

  const _radialScale = useMemo(() => (
    Math.min(1, Math.max(0.1, radialScale))
  ), [radialScale]);

  const _density = useMemo(() => (
    Math.min(1, Math.max(0.1, density))
  ), [density]);

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

  const dist = useCallback((x1, y1, x2, y2) => {
    return Math.hypot(x2 - x1, y2 - y1);
  }, []);

  const map = useCallback((value, start1, stop1, start2, stop2) => {
    const min = Math.min(start2, stop2);
    const max = Math.max(start2, stop2);
    const newValue = start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
    return Math.min(Math.max(newValue, min), max);
  }, []);

  const render = useCallback(() => {
    ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
    ctx.clearRect(0, 0, canvasWidth, canvasHeight);

    const dotContainerSize = (_gap * 2) + (_dotScale * 2);
    const diagonalDistance = Math.sqrt(width ** 2, height ** 2);

    for (let y = 0; y <= height; y += dotContainerSize) {
      for (let x = 0; x <= width; x += dotContainerSize) {
        if (variant === "random") {
          if (Math.random() > _density) {
            continue;
          }
        }
        ctx.beginPath();
        const posX = x + (dotContainerSize / 2);
        const posY = y + (dotContainerSize / 2);
        ctx.ellipse(posX, posY, _dotScale, _dotScale, 0, 0, 360, false);
        if (variant === "radial") {
          const distance = dist(posX, posY, (width / 2), (height / 2));
          let alpha = 1;
          if (radialDirection === "in") {
            alpha = map(distance, 0, (diagonalDistance / (10 * _radialScale)), 1, 0);
          } else {
            alpha = map(distance, diagonalDistance, (diagonalDistance / (2 / _radialScale)), 1, 0);
          }
          ctx.fillStyle = _dotColor(alpha);
        } else {
          ctx.fillStyle = dotColor;
        }
        ctx.fill();
        ctx.closePath();
      }
    }
  }, [
    ctx,
    map,
    dist,
    devicePixelRatio,
    width,
    height,
    canvasWidth,
    canvasHeight,
    variant,
    dotColor,
    radialDirection,
    _dotScale,
    _dotColor,
    _gap,
    _radialScale,
    _density,
  ]);

  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();
  }, [mounted, render]);

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

export default memo(ClassicDotsPatternBackground);

2. Copy the CSS module file

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

classic-dots-pattern-background.module.css

.classic-dots-pattern-background {
  position: relative;

  >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.

classic-dots-pattern-background-preview.jsx

import ClassicDotsPatternBackground from "@/components/backgrounds/classic-dots-pattern-background/classic-dots-pattern-background";

const ClassicDotsPatternBackgroundPreview = () => {
  return (
    <ClassicDotsPatternBackground
      variant="random"
      dotColor="#7a80dd"
      style={{
        width: "100%",
        height: "100%",
      }}
      wrapperProps = {{
        style: {
          display: "grid",
          placeItems: "center",
          height: "100%"
        }
      }}
    >
      <h2>
        Dot Pattern Background
      </h2>
    </ClassicDotsPatternBackground>
  );
};

export default ClassicDotsPatternBackgroundPreview;

Variants

classic-dots-pattern-background

import { useState } from "react";
import ClassicDotsPatternBackground from "@/components/backgrounds/classic-dots-pattern-background/classic-dots-pattern-background";

const ClassicDotsPatternBackgroundVariantsPreview = () => {
  const variants = ["standard", "radial", "random"];

  const [variant, setVariant] = useState("standard");
  
  return (
    <ClassicDotsPatternBackground
      variant={variant}
      dotColor="#7a80dd"
      style={classicDotPatternBackgroundStyles}
      wrapperProps = {{
        style: wrapperStyles
      }}
    >
      {variants.map(variantType => (
        <button
          key={variantType}
          onClick={() => setVariant(variantType)}
          style={{
            ...buttomStyles,
            ...(variantType === variant ? activeButtonStyles : null),
          }}
        >
          {variantType}
        </button>
      ))}
    </ClassicDotsPatternBackground>
  );
};

const classicDotPatternBackgroundStyles = {
  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 var(--text-primary)",
  borderRadius: "4px",
  background: "var(--layer-tertiary)",
  font: "inherit",
  color: "var(--text-primary)",
  textTransform: "uppercase",
};
const activeButtonStyles = {
  background: "#a4a8e7",
  color: "#111",
};

export default ClassicDotsPatternBackgroundVariantsPreview;

Customizations

classic-dots-pattern-background

import { useState } from "react";
import ClassicDotsPatternBackground from "@/components/backgrounds/classic-dots-pattern-background/classic-dots-pattern-background";

 const customizations = [
  {
    variant: "standard",
    gap: 2,
  },
   {
    variant: "standard",
    dotScale: 5,
    gap: 16,
  },
  {
    variant: "radial",
    dotScale: 1,
    gap: 2,
  },
  {
    variant: "radial",
    dotScale: 4,
    gap: 2,
  },
  {
    variant: "radial",
    dotScale: 2,
    gap: 2,
    radialDirection: "out",
  },
  {
    variant: "radial",
    dotScale: 5,
    gap: 1,
    radialDirection: "out",
    radialScale: 0.65,
  },
  {
    variant: "random",
    gap: 2,
    density: 0.8,
  },
  {
    variant: "random",
    gap: 8,
  },
];

const ClassicDotsPatternBackgroundCustomizationsPreview = () => {
  const [custmomizationIndex, setCustmomizationIndex] = useState(0);
  
  return (
    <ClassicDotsPatternBackground
      {...customizations[custmomizationIndex]}
      dotColor="#7a80dd"
      style={classicDotPatternBackgroundStyles}
      wrapperProps = {{
        style: wrapperStyles
      }}
    >
      {customizations.map((_, index) => (
        <button
          key={`customize-${index}`}
          onClick={() => setCustmomizationIndex(index)}
          style={{
            ...buttomStyles,
            ...(index === custmomizationIndex ? activeButtonStyles : null),
          }}
        >
          Pattern {index+1}
        </button>
      ))}
    </ClassicDotsPatternBackground>
  );
};

const classicDotPatternBackgroundStyles = {
  width: "100%",
  height: "480px",
  padding: "32px",
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
};
const wrapperStyles = {
  display: "flex",
  maxWidth: "480px",
  flexWrap: "wrap",
  justifyContent: "center",
  alignItems: "center",
  gap: "16px",
};
const buttomStyles = {
  padding: "8px 16px",
  border: "1px solid var(--text-primary)",
  borderRadius: "4px",
  background: "var(--layer-tertiary)",
  font: "inherit",
  color: "var(--text-primary)",
  textTransform: "uppercase",
};
const activeButtonStyles = {
  background: "#a4a8e7",
  color: "#111",
};

export default ClassicDotsPatternBackgroundCustomizationsPreview;

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
childrenReact.ReactNodeNoContent rendered inside the background wrapper.
variant"standard" | "radial" | "random"No"standard"Determines how dots are distributed in the background. standard renders a grid pattern, radial creates a radial fade effect, and random distributes dots randomly.
dotColorstringNo"rgb(255, 255, 255)"Color of the dots. Accepts rgb, rgba, or hex. For the radial variant, the alpha channel is used to produce the radial fade effect.
dotScalenumberNo1Controls the size of the dots. Recommended range: 0.1 – 5.
gapnumberNo4Space between dots in the grid. Minimum value: 1.
radialDirection"in" | "out"No"in"Direction of the radial effect when using the radial variant. in fades toward the center, out fades toward the edges.
radialScalenumberNo0.25Intensity/scale of the radial effect. Recommended range: 0.1 – 1. Only applies to the radial variant.
densitynumberNo0.5Controls how many dots appear in the random variant. Range: 0.1 – 1. Higher values produce denser patterns.
classNamestringNoAdditional class names applied to the background container.
styleReact.CSSPropertiesNoInline styles applied to the background container.
wrapperTagNamestringNo"div"HTML tag used as the wrapper element around the children.
wrapperPropsHTMLAttributes<HTMLElement>NoAdditional props passed to the wrapper element containing the children.