Wavy Threads
A smooth, noise-driven threads animation for adding subtle, organic motion to interfaces.
Wavy Threads Background
wavy-threads-background-preview.jsx
import WavyThreadsBackground from "@/components/backgrounds/wavy-threads-background/wavy-threads-background";
const WavyThreadsBackgroundPreview = () => {
return (
<WavyThreadsBackground
threadColor="rgba(162,201,229,0.65)"
style={{
width: "100%",
height: "100%"
}}
wrapperProps={{
style: {
height: "100%",
display: "grid",
placeItems: "center",
}
}}
>
<h2>Wavy Threads Background</h2>
</WavyThreadsBackground>
)
};
export default WavyThreadsBackgroundPreview; Installation
Run the command from your project root directory (the folder that contains package.json).
Terminal / Console
npx mosaicui-cli@latest backgrounds/wavy-threads-background 1. Copy the component file
Create a new file called wavy-threads-background.jsx
in your reusable components folder (for example
/src/components/) and paste the following
code into it.
wavy-threads-background.jsx
import { memo, useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from "react";
import styles from "./wavy-threads-background.module.css";
const WavyThreadsBackground = (props) => {
const {
children,
threadColor = "rgba(127, 127, 127, 0.5)",
threadCount = 10,
speed = 0.5,
amplitude = 50,
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 time = useRef(0);
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 { devicePixelRatio, canvasWidth, canvasHeight } = useMemo(() => {
const devicePixelRatio = Math.max(1, globalThis.devicePixelRatio || 1);
return {
devicePixelRatio,
canvasWidth: width * devicePixelRatio,
canvasHeight: height * devicePixelRatio,
};
}, [width, height]);
const noiseHashScale = 43758.5453123;
const lerp = useCallback((a, b, t) => a + (b - a) * t, []);
const fade = useCallback((t) => t * t * (3 - 2 * t), []);
const hash = useCallback((x) => {
const s = Math.sin(x * 127.1) * noiseHashScale;
return s - Math.floor(s);
}, [noiseHashScale]);
const noise = useCallback((x) => {
let i = Math.floor(x);
let f = x - i;
let a = hash(i);
let b = hash(i + 1);
return lerp(a, b, fade(f));
}, [lerp, fade, hash]);
const _threadCount = useMemo(() => (
Math.max(1, Math.min(100, threadCount))
), [threadCount]);
const _speed = useMemo(() => (
Math.max(0, Math.min(1, speed))
), [speed]);
const _amplitude = useMemo(() => (
Math.max(0, Math.min(100, amplitude))
), [amplitude]);
const threads = useMemo(() => (
Array.from({
length: _threadCount,
}).map(() => ({
xOffset: Math.random() * 1000,
amplitude: 10 + Math.random() * (_amplitude - 10),
speed: 0.01 + Math.random() * 0.03,
frequency: 0.01 + Math.random() * 0.03,
noiseScale: 0.002 + Math.random() * 0.006,
noiseStrength: 20 + Math.random() * 50
}))
), [_threadCount, _amplitude]);
const render = useCallback(() => {
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
const centerY = height / 2;
ctx.shadowBlur = 2;
ctx.shadowColor = threadColor;
ctx.lineWidth = 1.5;
ctx.strokeStyle = threadColor;
threads.forEach(thread => {
ctx.beginPath();
for (let x = 0; x <= width; x++) {
const t = x / width;
const envelope = Math.pow(Math.sin(Math.PI * t), 2);
const distortion = noise(x * 0.003 + time.current * 0.01) * 2;
const base = Math.sin(
x * thread.frequency +
time.current * thread.speed +
thread.xOffset +
distortion
) * thread.amplitude;
const n = ((
noise(x * thread.noiseScale + time.current * 0.02) * 0.7 +
noise(x * thread.noiseScale * 3 + time.current * 0.04) * 0.3
) - 0.5) * thread.noiseStrength;
const y = centerY + (base + n) * envelope;
if (x === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
});
time.current += _speed;
rafId.current = requestAnimationFrame(render);
}, [
ctx,
devicePixelRatio,
canvasWidth,
canvasHeight,
width,
height,
threads,
threadColor,
_speed,
noise,
]);
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-threads-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(WavyThreadsBackground); 2. Copy the CSS module file
In the same folder, create a file called wavy-threads-background.module.css and paste the following
CSS.
wavy-threads-background.module.css
.wavy-threads-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.
Speed
wavy-threads-background
import { useState } from "react";
import WavyThreadsBackground from "@/components/backgrounds/wavy-threads-background/wavy-threads-background";
const WavyThreadsBackgroundSpeedPreview = () => {
const [speed, setSpeed] = useState(0.5);
const handleSpeedChange = (e) => {
setSpeed(parseFloat(e.target.value));
};
return (
<WavyThreadsBackground
speed={speed}
style={backgroundStyles}
wrapperProps = {{
style: wrapperStyles,
}}
>
<label htmlFor="wavy-threads-speed-input">
Speed: {speed}
</label>
<input
type="range"
min="0"
max="1"
step="0.1"
id="wavy-threads-speed-input"
value={speed}
onChange={handleSpeedChange}
/>
</WavyThreadsBackground>
);
};
const backgroundStyles = {
width: "100%",
height: "100%",
display: "grid",
placeItems: "center",
};
const wrapperStyles = {
display: "grid",
placeItems: "center",
gap: "8px",
};
export default WavyThreadsBackgroundSpeedPreview; Thread Count
wavy-threads-background
import { useState } from "react";
import WavyThreadsBackground from "@/components/backgrounds/wavy-threads-background/wavy-threads-background";
const WavyThreadsBackgroundThreadCountPreview = () => {
const [threadCount, setThreadCount] = useState(10);
const handleThreadCountChange = (e) => {
setThreadCount(parseInt(e.target.value));
};
return (
<WavyThreadsBackground
threadCount={threadCount}
style={backgroundStyles}
wrapperProps = {{
style: wrapperStyles,
}}
>
<label htmlFor="wavy-threads-thread-count-input">
Thread Count: {threadCount}
</label>
<input
type="range"
min="1"
max="100"
step="1"
id="wavy-threads-thread-count-input"
value={threadCount}
onChange={handleThreadCountChange}
/>
</WavyThreadsBackground>
);
};
const backgroundStyles = {
width: "100%",
height: "100%",
display: "grid",
placeItems: "center",
};
const wrapperStyles = {
display: "grid",
placeItems: "center",
gap: "8px",
};
export default WavyThreadsBackgroundThreadCountPreview; Amplitude
wavy-threads-background
import { useState } from "react";
import WavyThreadsBackground from "@/components/backgrounds/wavy-threads-background/wavy-threads-background";
const WavyThreadsBackgroundAmplitudePreview = () => {
const [amplitude, setAmplitude] = useState(50);
const handleAmplitudeChange = (e) => {
setAmplitude(parseInt(e.target.value));
};
return (
<WavyThreadsBackground
amplitude={amplitude}
style={backgroundStyles}
wrapperProps = {{
style: wrapperStyles,
}}
>
<label htmlFor="wavy-threads-amplitude-input">
Amplitude: {amplitude}
</label>
<input
type="range"
min="1"
max="100"
step="1"
id="wavy-threads-amplitude-input"
value={amplitude}
onChange={handleAmplitudeChange}
/>
</WavyThreadsBackground>
);
};
const backgroundStyles = {
width: "100%",
height: "100%",
display: "grid",
placeItems: "center",
};
const wrapperStyles = {
display: "grid",
placeItems: "center",
gap: "8px",
};
export default WavyThreadsBackgroundAmplitudePreview; Props
Configure the component with the following props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | React.ReactNode | No | — | Content rendered inside the animated threads component. |
| threadColor | string | No | "rgba(127, 127, 127, 0.5)" | Color of the threads. Accepts HEX or RGBA values. |
| threadCount | number | No | 10 | Number of threads to render. Range: 1–100. |
| speed | number | No | 0.5 | Animation speed multiplier. Range: 0–1. |
| amplitude | number | No | 50 | Height/intensity of the wave distortion. Range: 0–100. |
| className | string | No | — | Optional CSS class 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. |