Wavy Dots
A subtle animated background where dots gently pulse toward the center in a harmonic wave motion.
Wavy Dots Background
wavy-dots-background-preview.jsx
import WavyDotsBackground from "@/components/backgrounds/wavy-dots-background/wavy-dots-background";
const WavyDotsBackgroundPreview = () => {
return (
<WavyDotsBackground
dotColor="rgba(162,201,229,0.5)"
style={{
width: "100%",
height: "100%"
}}
wrapperProps={{
style: {
height: "100%",
display: "grid",
placeItems: "center",
}
}}
>
<h2>Wavy Dots Background</h2>
</WavyDotsBackground>
)
};
export default WavyDotsBackgroundPreview; Installation
Run the command from your project root directory (the folder that contains package.json).
Terminal / Console
npx mosaicui-cli@latest backgrounds/wavy-dots-background 1. Copy the component file
Create a new file called wavy-dots-background.jsx
in your reusable components folder (for example
/src/components/) and paste the following
code into it.
wavy-dots-background.jsx
import { memo, useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from "react";
import styles from "./wavy-dots-background.module.css";
const map = (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);
};
class Dot {
constructor(x, y, cx, cy, cd, waves) {
const dx = cx - x;
const dy = cy - y;
this.x = x;
this.y = y;
this.length = Math.hypot(dx, dy);
this.dirX = dx / this.length;
this.dirY = dy / this.length;
this.time = map(this.length, 0, cd, 0, (Math.PI * 2 * waves));
this.magScale = map(this.length, 0, cd, 0.1, 0);
}
update(dt) {
this.time += dt;
this.time %= (Math.PI * 2);
}
getPosition() {
const wave = (Math.sin(this.time) + 1) / 2;
const offset = wave * this.magScale * this.length;
return {
x: this.x + this.dirX * offset,
y: this.y + this.dirY * offset,
};
}
draw(ctx, radius, dotColor) {
const p = this.getPosition();
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, (Math.PI * 2));
ctx.fillStyle = dotColor;
ctx.fill();
ctx.closePath();
}
}
const WavyDotsBackground = (props) => {
const {
children,
dotScale = 0.5,
dotColor = "rgba(127, 127, 127, 0.5)",
gap = 10,
speed = 0.5,
offset = 100,
className = "",
wrapperTagName = "div",
wrapperProps = {},
...restProps
} = props;
const {
className: wrapperClassName = "",
...restWrapperProps
} = wrapperProps;
const Wrapper = wrapperTagName || "div";
const containerRef = useRef(null);
const canvasRef = useRef(null);
const rafId = useRef(null);
const [mounted, setMounted] = useState(false);
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const ctx = useMemo(() => {
return canvasRef.current?.getContext("2d");
}, [canvasRef.current]);
const _dotRadius = useMemo(() => (
2 * Math.min(Math.max(0.1, dotScale), 5)
), [dotScale]);
const _gap = useMemo(() => (
Math.max(5, gap)
), [gap]);
const _speed = useMemo(() => (
0.005 + (0.1 * Math.min(Math.max(0.1, speed), 0.9))
), [speed]);
const { devicePixelRatio, canvasWidth, canvasHeight } = useMemo(() => {
const devicePixelRatio = Math.max(1, globalThis.devicePixelRatio || 1);
return {
devicePixelRatio,
canvasWidth: width * devicePixelRatio,
canvasHeight: height * devicePixelRatio,
};
}, [width, height]);
const dots = useMemo(() => {
const dots = [];
const wavesCount = 4;
const cw = width / 2;
const ch = height / 2;
const cd = Math.hypot(width, height);
const gapIncrement = _gap + (_dotRadius * 2);
for (let y = -offset; y <= (height + offset); y += gapIncrement) {
for (let x = -offset; x <= (width + offset); x += gapIncrement) {
dots.push(new Dot(x, y, cw, ch, cd, wavesCount));
}
}
return dots;
}, [width, height, _gap, _dotRadius, offset]);
const render = useCallback(() => {
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
dots.forEach(dot => {
dot.update(_speed);
dot.draw(ctx, _dotRadius, dotColor);
});
rafId.current = requestAnimationFrame(render);
}, [
ctx,
devicePixelRatio,
canvasWidth,
canvasHeight,
dots,
_speed,
_dotRadius,
dotColor,
]);
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["wavy-dots-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(WavyDotsBackground); 2. Copy the CSS module file
In the same folder, create a file called wavy-dots-background.module.css and paste the following
CSS.
wavy-dots-background.module.css
.wavy-dots-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;
}
} Examples
Discover how supported props transform components to match your needs.
Dot Scale
wavy-dots-background
import { useState } from "react";
import WavyDotsBackground from "@/components/backgrounds/wavy-dots-background/wavy-dots-background";
const WavyDotsBackgroundDotScalePreview = () => {
const [dotScale, setDotScale] = useState(1);
const handleDotScaleChange = (e) => {
setDotScale(parseFloat(e.target.value));
};
return (
<WavyDotsBackground
dotScale={dotScale}
style={backgroundStyles}
wrapperProps = {{
style: wrapperStyles,
}}
>
<label htmlFor="wavy-dots-scale-input">
Dot Scale: {dotScale}
</label>
<input
type="range"
min="0.1"
max="5"
step="0.1"
id="wavy-dots-scale-input"
value={dotScale}
onChange={handleDotScaleChange}
/>
</WavyDotsBackground>
);
};
const backgroundStyles = {
width: "100%",
height: "100%",
display: "grid",
placeItems: "center",
};
const wrapperStyles = {
display: "grid",
placeItems: "center",
gap: "8px",
};
export default WavyDotsBackgroundDotScalePreview; Speed
wavy-dots-background
import { useState } from "react";
import WavyDotsBackground from "@/components/backgrounds/wavy-dots-background/wavy-dots-background";
const WavyDotsBackgroundSpeedPreview = () => {
const [speed, setSpeed] = useState(0.5);
const handleSpeedChange = (e) => {
setSpeed(parseFloat(e.target.value));
};
return (
<WavyDotsBackground
speed={speed}
style={backgroundStyles}
wrapperProps = {{
style: wrapperStyles,
}}
>
<label htmlFor="wavy-dots-speed-input">
Speed: {speed}
</label>
<input
type="range"
min="0.1"
max="0.9"
step="0.1"
id="wavy-dots-speed-input"
value={speed}
onChange={handleSpeedChange}
/>
</WavyDotsBackground>
);
};
const backgroundStyles = {
width: "100%",
height: "100%",
display: "grid",
placeItems: "center",
};
const wrapperStyles = {
display: "grid",
placeItems: "center",
gap: "8px",
};
export default WavyDotsBackgroundSpeedPreview; Gap
wavy-dots-background
import { useState } from "react";
import WavyDotsBackground from "@/components/backgrounds/wavy-dots-background/wavy-dots-background";
const WavyDotsBackgroundGapPreview = () => {
const [gap, setGap] = useState(10);
const handleGapChange = (e) => {
setGap(parseFloat(e.target.value));
};
return (
<WavyDotsBackground
gap={gap}
style={backgroundStyles}
wrapperProps = {{
style: wrapperStyles,
}}
>
<label htmlFor="wavy-dots-gap-input">
Gap: {gap}
</label>
<input
type="range"
min="5"
max="20"
step="1"
id="wavy-dots-gap-input"
value={gap}
onChange={handleGapChange}
/>
</WavyDotsBackground>
);
};
const backgroundStyles = {
width: "100%",
height: "100%",
display: "grid",
placeItems: "center",
};
const wrapperStyles = {
display: "grid",
placeItems: "center",
gap: "8px",
};
export default WavyDotsBackgroundGapPreview; Props
Configure the component with the following props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | React.ReactNode | No | — | Content rendered above the animated background. |
| dotScale | number | No | 1 | Scale factor for each dot. Accepts values between 0.1 and 5. |
| dotColor | string | No | "rgba(127, 127, 127, 0.5)" | Color of the dots. Supports RGB, RGBA, or HEX color formats. |
| gap | number | No | 10 | Distance between dots in the grid (in px). Minimum value is 5. |
| speed | number | No | 0.5 | Speed of the oscillation animation. Accepts values between 0.1 and 0.9. |
| offset | number | No | 100 | Extra area (in px) outside the canvas where additional dots are generated to ensure the screen remains filled during motion. Reduce this if using radial masking near the corners. |
| className | string | No | "" | Additional CSS class names applied to the root container. |
| style | React.CSSProperties | No | — | Inline styles applied to the root container. |
| wrapperTagName | string | No | "div" | HTML tag used as the wrapper element around the children. |
| wrapperProps | React.HTMLAttributes<HTMLElement> | No | {} | Additional props passed to the wrapper element containing the children. |