Skip to main content

Sine Flow Background

An ambient wave band that ripples on its own: a requestAnimationFrame loop advances the phase of a six-crest sine and redraws a smooth cubic path, pausing whenever the band scrolls out of view and freezing flat under reduced motion. Fill, height, amplitude, and speed are props.

HeroReactMotionTailwind CSS
CSSTailwind

Manual

Create a file and paste the following code into it.

sine-flow-background.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"use client";

import { type CSSProperties, useEffect, useRef, useState } from "react";
import { useReducedMotion } from "motion/react";
import { cn } from "@/lib/cn";

/* ------------------------------------------------------------------ */
/*  SineFlowBackground                                                     */
/* ------------------------------------------------------------------ */

type WaveDirection = "up" | "down";

interface SineFlowBackgroundProps {
  /** Wave fill [Optional, default: currentColor via var(--foreground)] */
  fill?: string;
  background?: string;
  direction?: WaveDirection;
  /** Band height in px [Optional, default: 120] */
  height?: number;
  /** Phase speed multiplier [Optional, default: 1] */
  speed?: number;
  /** Crest amplitude in px [Optional, default: 28] */
  amplitude?: number;
  className?: string;
  style?: CSSProperties;
}

/** Six-crest wave path sampled from a sine at `phase`, drawn as smooth
 *  cubic segments across a fixed 1200-wide viewBox. */
function wavePath(phase: number, amplitude: number, direction: WaveDirection) {
  const baseline = direction === "down" ? 66 : 54;
  let d = `M-10,120 L-10,${baseline}`;
  for (let n = 0; n <= 6; n++) {
    const x = 200 * n;
    const y = baseline + Math.sin(phase + (n * Math.PI * 2) / 6) * amplitude;
    const ctrlY = baseline + Math.sin(phase + ((n - 0.5) * Math.PI * 2) / 6) * amplitude;
    if (n === 0) {
      d += ` L${x},${y}`;
    } else {
      const prevX = (n - 1) * 200;
      d += ` C${prevX + 100},${ctrlY} ${x - 100},${y} ${x},${y}`;
    }
  }
  return `${d} L1210,${baseline} L1210,120 Z`;
}

/**
 * An ambient wave band that ripples on its own: a requestAnimationFrame loop
 * advances the phase of a six-crest sine and redraws a smooth path, pausing
 * whenever the band scrolls out of view (IntersectionObserver) and freezing
 * flat under reduced-motion. Fill, height, amplitude, and speed are props.
 * @param {number} amplitude - Crest amplitude in px [Optional, default: 28]
 * @param {number} speed - Phase speed multiplier [Optional, default: 1]
 */
export function SineFlowBackground({
  fill = "var(--foreground)",
  background = "transparent",
  direction = "down",
  height = 120,
  speed = 1,
  amplitude = 28,
  className,
  style,
}: SineFlowBackgroundProps) {
  const reduceMotion = useReducedMotion();
  const [phase, setPhase] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);
  const prevTime = useRef(0);
  const rafId = useRef<number | null>(null);
  const visible = useRef(true);

  useEffect(() => {
    if (reduceMotion) return;
    const io = new IntersectionObserver(
      ([entry]) => {
        visible.current = entry.isIntersecting;
      },
      { threshold: 0 },
    );
    if (containerRef.current) io.observe(containerRef.current);

    const tick = (t: number) => {
      if (visible.current) {
        const dt = t - prevTime.current;
        setPhase((p) => p + (dt / 1000) * speed * 0.8);
      }
      prevTime.current = t;
      rafId.current = requestAnimationFrame(tick);
    };
    rafId.current = requestAnimationFrame((t) => {
      prevTime.current = t;
      rafId.current = requestAnimationFrame(tick);
    });
    return () => {
      if (rafId.current !== null) cancelAnimationFrame(rafId.current);
      io.disconnect();
    };
  }, [reduceMotion, speed]);

  const path = wavePath(reduceMotion ? 0 : phase, amplitude, direction);

  return (
    <div
      ref={containerRef}
      className={cn("relative overflow-hidden", className)}
      style={{ height, background, ...style }}
      aria-hidden="true"
    >
      <svg
        viewBox="0 0 1200 120"
        preserveAspectRatio="none"
        className="absolute inset-x-0 -inset-y-1 w-full"
      >
        <path d={path} fill={fill} />
      </svg>
    </div>
  );
}

Update the import paths to match your project setup.

Similar components

Glow Wave Bars

Starfall Field

Max

Grain Concierge Landing

Max

Pixel Atelier Landing

Install via CLI

Resource details

PublishedJuly 17, 2026
CategoryHero
ReactMotionTailwind CSS