Waves Background
Original · freelayered SVG wave background
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/bgx-waves.json"use client";
import { useId, useMemo, useState, type CSSProperties } from "react";
import { useReducedMotion } from "motion/react";
/* ---------------------------------------------------------------------------
* bgx-waves — layered SVG wave background
* Four parallax wave bands drift at independent speeds to build depth behind
* hero copy. Palette, motion speed and crest height are all live-controllable.
* ------------------------------------------------------------------------- */
type PaletteKey = "aurora" | "lagoon" | "ember";
interface PaletteDef {
label: string;
swatch: [string, string];
/** back → front fill colours (text-* so the SVG uses currentColor) */
colors: [string, string, string, string];
}
interface WaveShape {
ampBase: number;
wavelength: number;
phase: number;
baseline: number;
opacity: number;
baseDuration: number;
heightPct: number;
direction: "left" | "right";
}
/* viewBox is two tiles wide so translateX(-50%) loops seamlessly.
Every wavelength divides 1440 so each tile is perfectly periodic. */
const VIEW_W = 2880;
const VIEW_H = 320;
const LAYER_SHAPES: readonly WaveShape[] = [
{ ampBase: 26, wavelength: 720, phase: 0.0, baseline: 150, opacity: 0.4, baseDuration: 30, heightPct: 60, direction: "left" },
{ ampBase: 34, wavelength: 480, phase: 1.1, baseline: 186, opacity: 0.55, baseDuration: 23, heightPct: 68, direction: "right" },
{ ampBase: 42, wavelength: 360, phase: 2.4, baseline: 222, opacity: 0.72, baseDuration: 17, heightPct: 76, direction: "left" },
{ ampBase: 52, wavelength: 288, phase: 3.6, baseline: 256, opacity: 0.92, baseDuration: 12, heightPct: 84, direction: "right" },
];
const PALETTES: Record<PaletteKey, PaletteDef> = {
aurora: {
label: "Aurora",
swatch: ["bg-indigo-400", "bg-violet-400"],
colors: [
"text-indigo-200 dark:text-indigo-950",
"text-violet-300 dark:text-violet-900",
"text-indigo-400 dark:text-violet-800",
"text-violet-500 dark:text-indigo-700",
],
},
lagoon: {
label: "Lagoon",
swatch: ["bg-emerald-400", "bg-sky-400"],
colors: [
"text-emerald-200 dark:text-emerald-950",
"text-sky-300 dark:text-sky-900",
"text-emerald-400 dark:text-sky-800",
"text-sky-500 dark:text-emerald-700",
],
},
ember: {
label: "Ember",
swatch: ["bg-rose-400", "bg-amber-400"],
colors: [
"text-rose-200 dark:text-rose-950",
"text-amber-300 dark:text-amber-900",
"text-rose-400 dark:text-amber-800",
"text-amber-500 dark:text-rose-700",
],
},
};
const PALETTE_KEYS: readonly PaletteKey[] = ["aurora", "lagoon", "ember"];
/** Build a filled wave path across both tiles. Purely numeric — no React. */
function buildWavePath(amp: number, wavelength: number, phase: number, baseline: number): string {
const step = wavelength / 24;
const y0 = baseline - amp * Math.sin(phase);
let d = `M 0 ${VIEW_H} L 0 ${y0.toFixed(2)}`;
for (let x = step; x <= VIEW_W; x += step) {
const y = baseline - amp * Math.sin((x / wavelength) * Math.PI * 2 + phase);
d += ` L ${x.toFixed(2)} ${y.toFixed(2)}`;
}
d += ` L ${VIEW_W} ${VIEW_H} Z`;
return d;
}
const FOCUS_RING =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
export default function BgxWaves() {
const uid = useId();
const prefersReduced = !!useReducedMotion();
const [palette, setPalette] = useState<PaletteKey>("aurora");
const [speed, setSpeed] = useState(1.5);
const [crest, setCrest] = useState(60);
const [playing, setPlaying] = useState(true);
const paths = useMemo(
() =>
LAYER_SHAPES.map((s) =>
buildWavePath((s.ampBase * crest) / 100, s.wavelength, s.phase, s.baseline),
),
[crest],
);
const active = PALETTES[palette];
const speedId = `${uid}-speed`;
const crestId = `${uid}-crest`;
return (
<section className="relative w-full overflow-hidden bg-slate-50 dark:bg-slate-950">
<style>{`
@keyframes bgxwaves-drift-left {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(-50%, 0, 0); }
}
@keyframes bgxwaves-drift-right {
from { transform: translate3d(-50%, 0, 0); }
to { transform: translate3d(0, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
.bgxwaves-layer { animation: none !important; transform: none !important; }
}
`}</style>
{/* Wave field */}
<div className="pointer-events-none absolute inset-0" aria-hidden="true">
{LAYER_SHAPES.map((s, i) => {
const duration = s.baseDuration / speed;
const style: CSSProperties = prefersReduced
? {}
: {
animationName: s.direction === "left" ? "bgxwaves-drift-left" : "bgxwaves-drift-right",
animationDuration: `${duration.toFixed(2)}s`,
animationTimingFunction: "linear",
animationIterationCount: "infinite",
animationPlayState: playing ? "running" : "paused",
willChange: "transform",
};
return (
<svg
key={s.wavelength}
className={`bgxwaves-layer absolute bottom-0 left-0 w-[200%] ${active.colors[i]}`}
style={{ height: `${s.heightPct}%`, ...style }}
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
preserveAspectRatio="none"
focusable="false"
aria-hidden="true"
>
<path d={paths[i]} fill="currentColor" fillOpacity={s.opacity} />
</svg>
);
})}
</div>
{/* Legibility wash behind the copy */}
<div
className="pointer-events-none absolute inset-x-0 top-0 h-56 bg-gradient-to-b from-slate-50 via-slate-50/70 to-transparent dark:from-slate-950 dark:via-slate-950/70"
aria-hidden="true"
/>
{/* Content + controls */}
<div className="relative z-10 mx-auto flex min-h-[600px] max-w-3xl flex-col items-start justify-start px-6 py-20 sm:px-8 sm:py-24">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-300 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-wider text-slate-600 backdrop-blur dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300">
<span className={`h-1.5 w-1.5 rounded-full ${active.swatch[0]}`} />
Background · SVG
</span>
<h2 className="mt-5 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Layered wave background
</h2>
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-300">
Four parallax wave bands drift at independent speeds to build a sense of depth behind your
hero copy. Tune the palette, motion speed and crest height below and the field re-renders
in real time.
</p>
{/* Control panel */}
<div className="mt-8 w-full max-w-xl rounded-2xl border border-slate-200 bg-white/75 p-5 shadow-sm backdrop-blur-md sm:p-6 dark:border-slate-800 dark:bg-slate-900/75">
<div className="flex flex-col gap-6">
{/* Palette */}
<fieldset>
<legend className="mb-2 text-sm font-medium text-slate-700 dark:text-slate-200">
Colour palette
</legend>
<div className="flex flex-wrap gap-2">
{PALETTE_KEYS.map((key) => {
const p = PALETTES[key];
const selected = palette === key;
return (
<label key={key} className="relative cursor-pointer">
<input
type="radio"
name={`${uid}-palette`}
value={key}
checked={selected}
onChange={() => setPalette(key)}
className="peer sr-only"
/>
<span
className={`flex items-center gap-2 rounded-xl border px-3 py-2 text-sm font-medium transition peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-900 ${
selected
? "border-indigo-400 bg-indigo-50 text-indigo-900 dark:border-indigo-500 dark:bg-indigo-950 dark:text-indigo-100"
: "border-slate-200 bg-white text-slate-600 hover:border-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-slate-600"
}`}
>
<span className="flex -space-x-1">
<span className={`h-3 w-3 rounded-full ring-1 ring-white dark:ring-slate-900 ${p.swatch[0]}`} />
<span className={`h-3 w-3 rounded-full ring-1 ring-white dark:ring-slate-900 ${p.swatch[1]}`} />
</span>
{p.label}
</span>
</label>
);
})}
</div>
</fieldset>
{/* Speed */}
<div>
<div className="mb-2 flex items-center justify-between">
<label htmlFor={speedId} className="text-sm font-medium text-slate-700 dark:text-slate-200">
Motion speed
</label>
<span className="tabular-nums text-sm text-slate-500 dark:text-slate-400">
{speed.toFixed(1)}×
</span>
</div>
<input
id={speedId}
type="range"
min={0.5}
max={3}
step={0.5}
value={speed}
disabled={prefersReduced}
aria-valuetext={`${speed.toFixed(1)} times speed`}
onChange={(e) => setSpeed(Number(e.target.value))}
className={`w-full cursor-pointer accent-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 dark:accent-indigo-400 ${FOCUS_RING} rounded`}
/>
</div>
{/* Crest height */}
<div>
<div className="mb-2 flex items-center justify-between">
<label htmlFor={crestId} className="text-sm font-medium text-slate-700 dark:text-slate-200">
Crest height
</label>
<span className="tabular-nums text-sm text-slate-500 dark:text-slate-400">
{crest}%
</span>
</div>
<input
id={crestId}
type="range"
min={0}
max={100}
step={5}
value={crest}
aria-valuetext={`${crest} percent crest height`}
onChange={(e) => setCrest(Number(e.target.value))}
className={`w-full cursor-pointer accent-indigo-500 dark:accent-indigo-400 ${FOCUS_RING} rounded`}
/>
</div>
{/* Play / pause + status */}
<div className="flex flex-wrap items-center gap-3">
<button
type="button"
onClick={() => setPlaying((v) => !v)}
disabled={prefersReduced}
aria-pressed={playing}
aria-label={playing ? "Pause wave motion" : "Play wave motion"}
className={`inline-flex items-center gap-2 rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 ${FOCUS_RING}`}
>
{playing ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<rect x="3.5" y="2.5" width="3" height="11" rx="1" />
<rect x="9.5" y="2.5" width="3" height="11" rx="1" />
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M4 2.6v10.8a.8.8 0 0 0 1.22.68l8.4-5.4a.8.8 0 0 0 0-1.36l-8.4-5.4A.8.8 0 0 0 4 2.6Z" />
</svg>
)}
{playing ? "Pause" : "Play"}
</button>
<p className="text-sm text-slate-500 dark:text-slate-400" role="status">
{prefersReduced
? "Animation disabled by your system's reduced-motion setting."
: `${active.label} · ${playing ? "drifting" : "paused"}`}
</p>
</div>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Aurora Blobs Background
OriginalAn animated hero section with soft, ever-drifting aurora gradient blobs floating behind staggered, blur-in content.

Retro Perspective Grid
OriginalA launch and waitlist section set over an infinite neon grid that scrolls into a glowing horizon with rising particles.

Conic Gradient Flow
OriginalA frosted hero backed by twin counter-rotating conic gradients, with flowing gradient text and an animated gradient-border stat card.

Spotlight Follow Background
OriginalA dot-grid section where a spring-eased spotlight follows the cursor and reveals a hidden lattice of coloured dots.

Dot Pattern Background
pattern.tsxA reusable animated background for React and Tailwind: dot pattern background.

Flickering Grid Background
grid.tsxA reusable animated background for React and Tailwind: flickering grid background.

Grid Pattern Background
pattern.tsxA reusable animated background for React and Tailwind: grid pattern background.

Retro Grid Background
grid.jsonA reusable animated background for React and Tailwind: retro grid background.

Aurora Background
Originalaurora gradient background

Mesh Gradient Background
Originalanimated mesh gradient background

Grid Background
Originalgrid line background with fade

Dots Background
Originaldotted grid background

