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
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryHero
ReactMotionTailwind CSS
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryHero
ReactMotionTailwind CSS