Translucent waves layered along the bottom of a hero, each rolling at its own pace so they slide over one another like a calm sea.
Smooth sailing
Customize
Usage
import { WaveLayers } from "@/components/motionkit/wave-layers";
<WaveLayers
className="h-[36rem] rounded-3xl pt-32 text-center"
>
<h1>Your hero</h1>
</WaveLayers>Motion
The exact curves this component runs, plotted with Motion's own maths.
Front wave
Runs on its own
SaaS, travel, wellness and finance heroes, footers and sign-up pages: a calm, friendly finish along the bottom of a section.
Each layer is one SVG path two wavelengths wide, made from two sine waves added together so the shape isn't too regular. Sliding a layer left by exactly one wavelength lands it where it started, so a linear loop never shows a seam. Back layers are paler and slower, front layers stronger and quicker, which gives the parallax. Only CSS transforms move, so it stays smooth on any device.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://www.motionkitui.com/r/wave-layers.jsonnpm install motionSave as components/motionkit/wave-layers.tsx.
"use client";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type WaveLayersProps = {
children?: ReactNode;
/** One colour per layer, back to front. */
colors?: string[];
/** How much of the height the waves take up, 0 to 1. */
height?: number;
/** Seconds for the front wave to travel one wavelength; back layers are slower. 0 holds them still. */
duration?: number;
/** Waves along the bottom or hanging from the top. */
position?: "bottom" | "top";
className?: string;
};
const PERIOD = 1200;
/** A wave two periods wide, so sliding it one period left lands exactly where it started. */
function wavePath(amplitude: number, waves: number, phase: number) {
let d = `M0 ${300}`;
for (let x = 0; x <= PERIOD * 2; x += 20) {
const y = 150 + Math.sin((x / PERIOD) * Math.PI * 2 * waves + phase) * amplitude + Math.sin((x / PERIOD) * Math.PI * 2 * waves * 2 + phase * 1.7) * amplitude * 0.25;
d += ` L${x} ${y.toFixed(1)}`;
}
return `${d} L${PERIOD * 2} 300 Z`;
}
const layers = [
{ amplitude: 46, waves: 1, phase: 0.6, speed: 1.9, opacity: 0.35 },
{ amplitude: 38, waves: 2, phase: 2.1, speed: 1.4, opacity: 0.5 },
{ amplitude: 30, waves: 1, phase: 4.2, speed: 1.15, opacity: 0.7 },
{ amplitude: 24, waves: 2, phase: 5.3, speed: 1, opacity: 0.9 },
];
/**
* Translucent waves layered along the bottom of a hero, each rolling at its own pace so they slide over one
* another like a calm sea. SVG and CSS transforms only, so they stay smooth and cost almost nothing.
*/
export function WaveLayers({ children, colors = ["#073E45", "#0B6770", "#0E7C86", "#2E9098"], height = 0.45, duration = 14, position = "bottom", className = "" }: WaveLayersProps) {
const reduceMotion = useReducedMotion();
const still = reduceMotion || duration === 0;
return (
<div className={`relative isolate overflow-hidden bg-[#0C1010] ${className}`}>
<div aria-hidden className={`pointer-events-none absolute inset-x-0 -z-10 ${position === "bottom" ? "bottom-0" : "top-0 rotate-180"}`} style={{ height: `${height * 100}%` }}>
{layers.slice(0, Math.max(1, colors.length)).map((layer, index) => (
<motion.svg
key={index}
viewBox={`0 0 ${PERIOD * 2} 300`}
preserveAspectRatio="none"
className="absolute bottom-0 left-0 h-full w-[200%]"
style={{ opacity: layer.opacity }}
animate={still ? undefined : { x: ["0%", "-50%"] }}
transition={{ duration: duration * layer.speed, repeat: Infinity, ease: "linear" }}
>
<path d={wavePath(layer.amplitude, layer.waves, layer.phase)} fill={colors[index % colors.length]} />
</motion.svg>
))}
</div>
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| colors | string[] | ["#073E45", "#0B6770", "#0E7C86", "#2E9098"] | One colour per layer, back to front (up to four). |
| height | number | 0.45 | Share of the height the waves fill. |
| duration | number | 14 | Seconds per wavelength for the front wave. 0 holds them still. |
| position | "bottom" | "top" | "bottom" | Along the bottom, or hanging from the top. |
| children | ReactNode | — | Your content, layered above. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Only the motion package, beside React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://www.motionkitui.com/r/wave-layers.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, the waves hold still.