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:

PropTypeRequiredDefaultDescription
childrenReact.ReactNodeNoContent rendered inside the animated threads component.
threadColorstringNo"rgba(127, 127, 127, 0.5)"Color of the threads. Accepts HEX or RGBA values.
threadCountnumberNo10Number of threads to render. Range: 1–100.
speednumberNo0.5Animation speed multiplier. Range: 0–1.
amplitudenumberNo50Height/intensity of the wave distortion. Range: 0–100.
classNamestringNoOptional CSS class applied to the root container.
styleReact.CSSPropertiesNoInline styles applied to the root container.
wrapperTagNamestringNo"div"HTML tag used as the wrapper element around the children.
wrapperPropsReact.HTMLAttributes<HTMLElement>No{} Additional props passed to the wrapper element containing the children.