# Wave Layers

> 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.

Category: Backgrounds · Free · Updated 2026-10-30 · Live demo: https://www.motionkitui.com/components/wave-layers

## When to use it

SaaS, travel, wellness and finance heroes, footers and sign-up pages: a calm, friendly finish along the bottom of a section.

## How it works

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.

## Installation

```bash
npx shadcn@latest add https://www.motionkitui.com/r/wave-layers.json
```

Or copy `components/motionkit/wave-layers.tsx` below. Requires react, motion and Tailwind CSS v4. Free for personal projects; commercial use needs MotionKit Plus (https://www.motionkitui.com/pricing).

## Props

| 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. |

## Accessibility

- **Keyboard: Not applicable.** Decorative, nothing to focus.
- **Screen readers: Pass.** The waves are hidden from assistive tech; only your content is read.
- **Reduced motion: Pass.** With reduced motion on, the waves hold still.
- **Colour contrast: Pass.** Text above the waves sits on the plain background: ivory measures 17:1.

## Source

```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>
  );
}
```
