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:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | React.ReactNode | No | — | Content 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. |
| dotColor | string | No | "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. |
| dotScale | number | No | 1 | Controls the size of the dots. Recommended range: 0.1 – 5. |
| gap | number | No | 4 | Space 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. |
| radialScale | number | No | 0.25 | Intensity/scale of the radial effect. Recommended range: 0.1 – 1. Only applies to the radial variant. |
| density | number | No | 0.5 | Controls how many dots appear in the random variant. Range: 0.1 – 1. Higher values produce denser patterns. |
| className | string | No | — | Additional class names applied to the background container. |
| style | React.CSSProperties | No | — | Inline styles applied to the background container. |
| wrapperTagName | string | No | "div" | HTML tag used as the wrapper element around the children. |
| wrapperProps | HTMLAttributes<HTMLElement> | No | — | Additional props passed to the wrapper element containing the children. |